import { EventEmitter } from 'events' ; import { Agent } from '../Agent' ; import { Match } from '../Match' ; import { Design } from '../Design' ; import { FatalError , TournamentPlayerDoesNotExistError , DatabaseGetUserError , } from '../DimensionError' ; import { DeepPartial } from '../utils/DeepPartial' ; import { Logger } from '../Logger' ; import { RankSystem as _RankSystem } from './RankSystem' ; import { deepCopy } from '../utils/DeepCopy' ; import { Dimension , NanoID } from '../Dimension' ; import { genID } from '../utils' ; import { nanoid } from '..' ; import { removeDirectorySync } from '../utils/System' ; import { Database } from '../Plugin/Database' ; import VarLock from '../utils/VarLock' ; import { TournamentStatus as _TournamentStatus } from './TournamentStatus' ; import { TournamentType as _TOURNAMENT_TYPE } from './TournamentTypes' ; ignore ignore ignore /**
 * Player class that persists data for the same ephemereal agent across multiple matches. Used for  Tournament export class Player { /**
     * Whether this player is anonymous and not tied to a user on the back end
     *
     * If this is ever false, then that means 1. we have a backend setup 2. there is an actual user entry
     */ public anonymous = true ; /** Associated username if there is one */ public username : string = undefined ; /**
     * Path to player's directory, not the file to be executed/used. Value is necessary if storage system is not used or
     * there is no bot key because the player is anonymous (locally stored). Used so tournament can clean up old bot
     * directory when a new bot is uploaded
     */ public botDirPath : string = undefined ; /**
     * Key that references the player's bot file object if it exists
     */ public botkey : string = undefined ; /**
     * Whether or not this player is disabled and won't be used in in the default match scheduling for
     *  Tournament . Ladder . Is set to true if this player's bot throws an error during
     * the initialization stage of a  Match .
      public disabled = false ; /**
     * Path to the zip file for the bot. Used when no storage service is used. Used to allow api to send zipped bot file
     */ public zipFile : string = undefined ; /**
     * The version of the bot associated with this player. Incremented whenever addPlayer is called
     */ public version = 0 ; constructor ( public tournamentID , public file , zipFile , botkey ? ) { this . botkey = botkey ; this . zipFile = zipFile ; } /**
     * Generates a 12 character player id string
     */ public static generatePlayerID ( ) { return genID ( 12 ) ; } } /**
 * The tournament class and module extended by all concrete Tournament Classes. Tournament Types available now are
 *  Ladder , and  Elimination . A tournament is composed of players, which can either be
 * all locally stored, or a split between locally stored anonymous players and database stored user owned players.
 * Ladder is the only tournament where it can be made distributed, other tournament types may only be run as a single
 * instance
 *
 * Notes: `this.competitors` map is used when no DB is used. When a DB is used, locally stored players are only in
 * `this.anonymousCompetitors` and other players are pulled from DB. Hence, a lot of code requires checking if database
 * exists and if so, pull from there and the anonymous competitors map, other wise use this.state or this.competitors
  export abstract class Tournament extends EventEmitter { /** Tournament configs */ abstract configs : Tournament . TournamentConfigsBase ; /** Mapping match ids to active ongoing matches */ public matches : Map < NanoID , Match > = new Map ( ) ; /** A queue whose elements are each arrays of players that are to compete against each other */ public matchQueue : Array < Tournament . QueuedMatch > = [ ] ; /** The current status of the tournament */ public status : Tournament . Status = Tournament . Status . UNINITIALIZED ; /** Ongoing tournament state. Type dependent on Tournament Type chosen */ abstract state : Tournament . TournamentTypeState ; /** Logger */ public log = new Logger ( ) ; /** Registered competitors in this tournament */ public competitors : Map < NanoID , Player > = new Map ( ) ; /**
     * All competitors that are anonymous, local (no user), competitors and not registered in database. Used only when
     * there is a DB
     */ public anonymousCompetitors : Map < NanoID , Player > = new Map ( ) ; /** A reference to the dimension this tournament was spawned from */ public dimension : Dimension ; /** This tournament's ID */ public id : NanoID ; /**
     * This Tournament's name
     */ public name = '' ; /** Tournament Type */ abstract type : Tournament . Type ; /**
     * Promise array of which all resolves once every player added through constructor is finished adding
     */ public initialAddPlayerPromises : Array < Promise < any > > = [ ] ; /**
     * Map from player ids to a promise that resolves once it is unlocked from the function that initiated a promise
     */ private lockedPlayers : Map < nanoid , VarLock > = new Map ( ) ; /**
     * Map from player ids to the last version used in a match in this tournament instance
     *
     * TODO: This is not a good solution, it is effectively an in-memory cache used to invalidate past bot files. It has
     * low space overhead but it won't scale nicely.
     */ private lastVersionUsed : Map < nanoid , number > = new Map ( ) ; constructor ( protected design , id , tournamentConfigs , dimension ) { super ( ) ; this . id = id ; if ( tournamentConfigs . id ) { this . id = tournamentConfigs . id ; } this . log . level = tournamentConfigs . loggingLevel !== undefined ? tournamentConfigs . loggingLevel : Logger . LEVEL . INFO ; this . name = tournamentConfigs . name ? tournamentConfigs . name : `tournament_${ this . id }` ; this . log . identifier = this . name ; this . dimension = dimension ; this . log . info ( `Created Tournament - ID: ${ this . id }, Name: ${ this . name }` ) ; if ( ! tournamentConfigs . name && dimension . hasDatabase ( ) ) { this . log . error ( `A name has to be specified for a tournament otherwise tournament player data will not be reused across runs of the tournament` ) ; } } param file param existingID param calledFromInitialization public async addplayer ( file , existingID ? , calledFromInitialization = false ) { let id ; if ( typeof file !== 'string' ) { if ( ! existingID ) { existingID = file . existingID ; } } if ( existingID ) { const { playerStat } = await this . getPlayerStat ( existingID ) ; if ( playerStat ) { let varlock = this . lockedPlayers . get ( existingID ) ; if ( varlock ) { await varlock . lockvar ( ) ; } else { varlock = new VarLock ( ) ; this . lockedPlayers . set ( existingID , varlock ) ; } const player = playerStat . player ; player . disabled = false ; const oldname = player . tournamentID . name ; const oldfile = player . file ; if ( player . botDirPath ) { removeDirectorySync ( player . botDirPath ) ; } if ( typeof file === 'string' ) { player . file = file ; } else { player . file = file . file ; player . tournamentID . name = file . name ; player . botDirPath = file . botdir ; player . zipFile = file . zipFile ; player . botkey = file . botkey ; if ( ! calledFromInitialization ) { player . version ++ ; } } try { await this . updatePlayer ( player , oldname , oldfile ) ; varlock . unlock ( ) ; this . lockedPlayers . delete ( existingID ) ; } catch ( err ) { varlock . unlockWithError ( err ) ; this . lockedPlayers . delete ( existingID ) ; throw err ; } id = existingID ; return player ; } else { id = existingID ; } } else { id = this . generateNextTournamentIDString ( ) ; } if ( typeof file === 'string' ) { const name = `player-${ id }` ; const newPlayer = new Player ( { id : id , name : name , username : undefined } , file , undefined ) ; if ( this . dimension . hasDatabase ( ) ) { const user = await this . dimension . databasePlugin . getUser ( newPlayer . tournamentID . id ) ; if ( user ) { newPlayer . anonymous = false ; newPlayer . username = user . username ; newPlayer . tournamentID . username = user . username ; } else { this . competitors . set ( id , newPlayer ) ; } } else { this . competitors . set ( id , newPlayer ) ; } if ( newPlayer . anonymous ) { this . anonymousCompetitors . set ( id , newPlayer ) ; } await this . internalAddPlayer ( newPlayer ) ; return newPlayer ; } else { const newPlayer = new Player ( { id : id , name : file . name , username : undefined } , file . file , file . zipFile , file . botkey ) ; newPlayer . botDirPath = file . botdir ; if ( this . dimension . hasDatabase ( ) ) { const user = await this . dimension . databasePlugin . getUser ( newPlayer . tournamentID . id ) ; if ( user ) { newPlayer . tournamentID . name = file . name ; newPlayer . anonymous = false ; newPlayer . username = user . username ; newPlayer . tournamentID . username = user . username ; } else { this . competitors . set ( id , newPlayer ) ; } } else { this . competitors . set ( id , newPlayer ) ; } if ( newPlayer . anonymous ) { this . anonymousCompetitors . set ( id , newPlayer ) ; } await this . internalAddPlayer ( newPlayer ) ; return newPlayer ; } } param player abstract async internalAddPlayer ( player ) ; /**
     * Returns a new id for identifying a player in a tournament
     * Only used when adding a plyaer to a tournament is done without specifying an id to use.
     */ public generateNextTournamentIDString ( ) { return Player . generatePlayerID ( ) ; } param configs param master public abstract async run ( configs ? , master ? ) ; param master public abstract async stop ( master ? ) ; param master public abstract async resume ( master ? ) ; /**
     * Retrieve some form of rankings from the tournament's current state. The params offset and limit only apply to
     *  Tournament . Ladder , used for scaling purposes.
     *
     *  param offset param limit public abstract getRankings ( offset ? , limit ? ) ; param player - the  Player that was updated
     *  param oldname param oldfile abstract async updatePlayer ( player , oldname , oldfile ) ; param playerID public async disablePlayer ( playerID ) { const { user , playerStat } = await this . getPlayerStat ( playerID ) ; if ( playerStat ) { playerStat . player . disabled = true ; if ( this . dimension . hasDatabase ( ) && user ) { await this . dimension . databasePlugin . updateUser ( playerID , user ) ; } } else { throw new TournamentPlayerDoesNotExistError ( `Player ${ playerID } was not found in this tournament` ) ; } } /**
     * Removes the competitor/player with id `playerID` (a  nanoid ). Resolves if succesful, otherwise rejects if
     * player doesn't exist or couldn't be removed
     *
     *  param playerID public async removePlayer ( playerID ) { const { user , playerStat } = await this . getPlayerStat ( playerID ) ; if ( playerStat ) { this . competitors . delete ( playerID ) ; this . anonymousCompetitors . delete ( playerID ) ; playerStat . player . disabled = true ; if ( this . dimension . hasDatabase ( ) && user ) { await this . dimension . databasePlugin . updateUser ( playerID , user ) ; } await this . internalRemovePlayer ( playerID ) ; } else { throw new TournamentPlayerDoesNotExistError ( 'Not a player' ) ; } } protected async internalRemovePlayer ( playerID ) { } param configs abstract setConfigs ( configs ) ; param configs abstract getConfigs ( ) ; param players returns protected async runMatch ( players ) { if ( ! players . length ) throw new FatalError ( 'No players provided for match' ) ; const locks = [ ] ; players . forEach ( ( player ) => { if ( this . lockedPlayers . has ( player . tournamentID . id ) ) { locks . push ( this . lockedPlayers . get ( player . tournamentID . id ) . lock ) ; } } ) ; const matchConfigs = deepCopy ( this . getConfigs ( ) . defaultMatchConfigs ) ; const agentSpecificOptions = [ ] ; for ( const player of players ) { const lv = await this . getLastVersionUsedOfPlayer ( player . tournamentID . id ) ; if ( lv !== undefined && lv === player . version ) { this . log . system ( `player ${ player . tournamentID . id } attempting to use cached bot` ) ; agentSpecificOptions . push ( { useCachedBotFile : true , } ) ; } else { this . log . system ( `player ${ player . tournamentID . id } not using cached bot file` ) ; agentSpecificOptions . push ( { useCachedBotFile : false , } ) ; } } matchConfigs . agentSpecificOptions = agentSpecificOptions ; const filesAndNamesAndIDs = players . map ( ( player ) => { return { file : player . file , tournamentID : player . tournamentID , botkey : player . botkey , version : player . version , } ; } ) ; const match = new Match ( this . design , filesAndNamesAndIDs , matchConfigs , this . dimension ) ; this . matches . set ( match . id , match ) ; try { /**
             * using "mutex" locks to force any update to players to happen completely before or after match initialization,
             * helping eliminate race conditions
             */ await Promise . all ( locks ) ; for ( let i = 0 ; i < players . length ; i ++ ) { const varlock = this . lockedPlayers . get ( players [ i ] . tournamentID . id ) ; if ( varlock ) { await varlock . lockvar ( ) ; } else { this . lockedPlayers . set ( players [ i ] . tournamentID . id , new VarLock ( ) ) ; } } await match . initialize ( ) . finally ( ( ) => { for ( let i = 0 ; i < players . length ; i ++ ) { const varlock = this . lockedPlayers . get ( players [ i ] . tournamentID . id ) ; if ( varlock ) { varlock . unlock ( ) ; this . lockedPlayers . delete ( players [ i ] . tournamentID . id ) ; } } } ) ; players . forEach ( async ( player ) => { await this . updateLastVersionUsedOfPlayer ( player . tournamentID . id , player . version ) ; } ) ; const results = await match . run ( ) ; this . matches . delete ( match . id ) ; return { results , match } ; } catch ( err ) { this . emit ( Tournament . Events . MATCH_RAN ) ; return { results : false , err , match , } ; } finally { if ( this . dimension . hasDatabase ( ) ) { if ( this . dimension . databasePlugin . configs . saveTournamentMatches ) { this . dimension . databasePlugin . storeMatch ( match , this . id ) ; } } } } param queuedMatchInfo public async getMatchInfoFromQueuedMatch ( queuedMatchInfo ) { const retrievePlayerPromises = [ ] ; for ( let i = 0 ; i < queuedMatchInfo . length ; i ++ ) { const playerId = queuedMatchInfo [ i ] ; retrievePlayerPromises . push ( this . getPlayerStat ( playerId ) . then ( ( { playerStat } ) => playerStat . player ) ) ; } return await Promise . all ( retrievePlayerPromises ) ; } public async getLastVersionUsedOfPlayer ( playerID ) { return Promise . resolve ( this . lastVersionUsed . get ( playerID ) ) ; } public async updateLastVersionUsedOfPlayer ( playerID , version ) { this . lastVersionUsed . set ( playerID , version ) ; return ; } /**
     * Removes a match by id. Returns true if deleted, false if nothing was deleted
     */ public async removeMatch ( matchID ) { if ( this . matches . has ( matchID ) ) { const match = this . matches . get ( matchID ) ; await match . destroy ( ) ; return this . matches . delete ( matchID ) ; } return false ; } /**
     * Destroy this tournament. Rejects if an error occured in trying to destroy it.
     */ public async destroy ( ) { await this . preInternalDestroy ( ) ; if ( this . status === Tournament . Status . RUNNING ) this . stop ( ) ; const destroyPromises = [ ] ; this . matches . forEach ( ( match ) => { destroyPromises . push ( match . destroy ( ) ) ; } ) ; await Promise . all ( destroyPromises ) ; await this . postInternalDestroy ( ) ; } /**
     * Pre run function before generic destroy takes place
     */ protected async preInternalDestroy ( ) { } /**
     * Post run function before generic destroy takes place
     */ protected async postInternalDestroy ( ) { } /**
     * Generates a 6 character tournament ID identifying this tournament class instance. Not to be confused with
     *  Tournament . ID which is the ID for competitors in the tournament
      public static genTournamentClassID ( ) { return genID ( 6 ) ; } /**
     * Returns the name of the tournament but formatted (no spaces)
     */ public getSafeName ( ) { return this . name . replace ( / /g , '_' ) ; } /**
     * Returns a key name to be used when storing a tournament by a combination of its name and id
     */ public getKeyName ( ) { return `${ this . getSafeName ( ) }_${ this . id }` ; } param id public async getPlayerStat ( id ) { if ( ! this . state . playerStats . has ( id ) ) { if ( this . dimension . hasDatabase ( ) ) { let user ; try { user = await this . dimension . databasePlugin . getUser ( id ) ; } catch ( err ) { this . log . error ( err ) ; throw new DatabaseGetUserError ( `failed to get database user with id: ${ id }` ) ; } if ( user && user . statistics [ this . getKeyName ( ) ] ) { return { user : user , playerStat : user . statistics [ this . getKeyName ( ) ] } ; } } } else { return { user : null , playerStat : this . state . playerStats . get ( id ) } ; } return { user : null , playerStat : null } ; } } import { Ladder as LadderTournament } from './Ladder' ; ignore import { Elimination as EliminationTournament } from './Elimination' ; ignore import { Scheduler as SchedulerClass } from './Scheduler' ; ignore export namespace Tournament { export import Ladder = LadderTournament ; export import Elimination = EliminationTournament ; export import Scheduler = SchedulerClass ; export import Type = _TOURNAMENT_TYPE ; export import Status = _TournamentStatus ; export import RankSystem = _RankSystem ; /**
     * Required and Optional Tournament configurations
     */ export interface TournamentConfigsBase { /**
         * The default match configurations to be applied throughout all tournament matches
         */ defaultMatchConfigs ? : DeepPartial < Match . Configs > ; /**
         * The tournament type to run. See  Tournament . Type type : Type ; /**
         * The ranking system to use for this tournament. Either a string or a class that extends RankSystem
         */ rankSystem : RankSystem < any , any > | Tournament . RankSystemTypes ; /**
         * The result handler for returning the appropriate results to the tournament for processing.
         *
         * To find what kind of result should be returned, find the Results interface for the rank system you are using.
         *
         * Example: For  Tournament . RankSystem . TRUESKILL , go to  Tournament . RankSystem . TRUESKILL . Results resultHandler : ( results : any ) => any ; /**
         * The configurations for a specified rank system. For example, see  RankSystem . WINS . Configs ,
         *  RankSystem . TRUESKILL . Configs rankSystemConfigs ? : any ; /**
         * The tournament wide logging level to enforce
         */ loggingLevel ? : Logger . LEVEL ; /**
         * The name of the tournament
         */ name ? : string ; /**
         * Tournament configurations. Dependent on the type of tournament chosen
         * Example: For  Ladder , go to  Ladder . Configs tournamentConfigs ? : any ; default agentsPerMatch : Array < number > ; default consoleDisplay ? : boolean ; /**
         * Set this ID to override the generated ID
         */ id ? : string ; } /**
     * Queued match information, consisting of player IDs of players to compete
     */ export type QueuedMatch = Array < nanoid > ; /**
     * Internally used type.
     */ export interface TournamentConfigs < ConfigType > extends TournamentConfigsBase { tournamentConfigs : ConfigType ; rankSystemConfigs : any ; } export interface TournamentTypeConfig { default storePastResults : boolean ; } export interface TournamentTypeState { /**
         * Past results stored. Each element is what is returned by  Design . getResults results : Array < any > ; /**
         * Map from player ID to player stats
         */ playerStats : Map < NanoID , PlayerStatBase > ; } /**
     * Tournament.ID. Represents an identifier for players competing in a  Tournament export interface ID { /** A string id. This should never change */ readonly id : NanoID ; /** A display name */ name : string ; /** Associated username if there is one. */ username : string ; } export interface PlayerStatBase { player : Player ; matchesPlayed : number ; } export enum Events { /**
         * Event is emitted when initialAddPlayerPromises resolves. This involves all players initialized to tournament
         * upon initialization of tournament instance
         */ INITIAL_PLAYERS_INITIALIZED = 'initial_players_initialized' , /**
         * Event is emitted whenever a match runs
         */ MATCH_RAN = 'match_ran' , /**
         * Event is emitted whenever a match runs AND the tournament is completely done handling it.
         */ MATCH_HANDLED = 'match_handled' } export enum RankSystemTypes { /** Ranking by wins, ties and losses */ WINS = 'wins' , /** Ranking by the ELO ranking system */ ELO = 'elo' , /** Ranking by Microsoft's Trueskill */ TRUESKILL = 'trueskill' } }