import { DeepPartial } from '../utils/DeepPartial' ; import { deepMerge } from '../utils/DeepMerge' ; import { MatchEngine } from '../MatchEngine' ; import { Agent } from '../Agent' ; import { Logger } from '../Logger' ; import { Design } from '../Design' ; import { FatalError , MatchDestroyedError , MatchWarn , MatchError , NotSupportedError , MatchReplayFileError , AgentError , AgentCompileError , AgentInstallError , } from '../DimensionError' ; import { Tournament } from '../Tournament' ; import EngineOptions = MatchEngine . EngineOptions ; import COMMAND_STREAM_TYPE = MatchEngine . COMMAND_STREAM_TYPE ; import Command = MatchEngine . Command ; import { ChildProcess } from 'child_process' ; import { NanoID , Dimension } from '../Dimension' ; import { genID } from '../utils' ; import { deepCopy } from '../utils/DeepCopy' ; import path from 'path' ; import extract from 'extract-zip' ; import { removeDirectory , removeFile } from '../utils/System' ; import { BOT_DIR } from '../Station' ; import { mkdirSync , existsSync , statSync } from 'fs' ; /**
 * An match created using a  Design and a list of Agents. The match can be stopped and resumed with
 *  stop ,  resume , and state and configurations can be retrieved at any point in time with the
 *  state and  configs fields
 *
 *  see Design for Design information
 *  see Agent for Agent information
  export class Match { /**
     * When the match was created (called with new)
     */ public creationDate : Date ; /**
     * When the match finished
     */ public finishDate : Date ; /**
     * Name of the match
     */ public name : string ; /**
     * Match ID. It's always a 12 character NanoID
     */ public id : NanoID ; /**
     * The state field. This can be used to store anything by the user when this `match` is passed to the  Design * life cycle functions  Design . initialize ,  Design . update , and  Design . getResults *
     * This is also used by the  CustomDesign class to store all standard outputted match output for
     * matches running using a custom design and passing in  Design . OverrideOptions .
      public state : any ; See Agent for details on the agents.
      public agents : Array < Agent > = [ ] ; /**
     * Map from an  Agent . ID ID to the  Agent public idToAgentsMap : Map < Agent . ID , Agent > = new Map ( ) ; /**
     * The current time step of the Match. This time step is independent of any  Design and agents are coordianted
     * against this timeStep
      public timeStep = 0 ; /**
     * The associated  MatchEngine that is running this match and serves as the backend for this match.
      public matchEngine : MatchEngine ; see Logger for details on how to use this
      public log = new Logger ( ) ; /**
     * The results field meant to store any results retrieved with  Design . getResults default public results : any = null ; /**
     * The current match status
     */ public matchStatus : Match . Status = Match . Status . UNINITIALIZED ; /**
     * A mapping from  Agent IDs to the tournament id of the  Player in a tournament that generated the
     *  Agent public mapAgentIDtoTournamentID : Map < Agent . ID , Tournament . ID > = new Map ( ) ; /**
     * Match Configurations. See  Match . Configs for configuration options
      public configs : Match . Configs = { name : '' , loggingLevel : Logger . LEVEL . INFO , engineOptions : { } , secureMode : false , agentOptions : deepCopy ( Agent . OptionDefaults ) , languageSpecificAgentOptions : { } , storeReplay : true , storeReplayDirectory : 'replays' , storeErrorLogs : true , storeErrorDirectory : 'errorlogs' , agentSpecificOptions : [ ] , storeMatchErrorLogs : false , detached : false , } ; /** Match process used to store the process governing a match running on a custom design */ matchProcess : ChildProcess ; /** The timer set for the match process */ matchProcessTimer : any ; /** Signal to stop at next time step */ private shouldStop = false ; /** Promise for resuming */ private resumePromise : Promise < void > ; /** Resolver for the above promise */ private resumeResolve : Function ; /** Resolver for stop Promise */ private resolveStopPromise : Function ; /** Rejecter for the run promise */ private runReject : Function ; /**
     * Path to the replay file for this match
     */ public replayFile : string ; /**
     * Key used to retrieve the replay file from a storage plugin
     */ public replayFileKey : string ; /**
     * Non local files that should be removed as they are stored somewhere else. Typically bot files are non local if
     * using a backing storage service
     */ private nonLocalFiles : Array < string > = [ ] ; param design - The  Design used
     *  param agents param configs - Configurations that are passed to every run through  Design . initialize ,  Design . update , and  Design . getResults functioon in the
     * given  Design constructor ( public design , /**
     * agent meta data regarding files, ids, etc.
     */ public agentFiles , configs = { } , private dimension ) { this . configs = deepMerge ( deepCopy ( this . configs ) , deepCopy ( configs ) ) ; this . configs . agentOptions . secureMode = this . configs . secureMode ; this . configs . agentOptions . loggingLevel = this . configs . loggingLevel ; this . id = Match . genMatchID ( ) ; this . creationDate = new Date ( ) ; if ( this . configs . name ) { this . name = this . configs . name ; } else { this . name = `match_${ this . id }` ; } this . log . level = this . configs . loggingLevel ; this . log . identifier = this . name ; this . matchEngine = new MatchEngine ( this . design , this . log . level ) ; this . matchEngine . setEngineOptions ( configs . engineOptions ) ; } /**
     * Initializes this match using its configurations and using the  Design . initialize function. This can
     * throw error with agent generation, design initialization, or with engine initialization. In engine initialization,
     * errors that can be thrown can be  AgentCompileError ,
     *  AgentInstallError , etc.
     *
     *
     *  returns public async initialize ( ) { try { this . log . infobar ( ) ; this . log . info ( `Design: ${ this . design . name } | Initializing match - ID: ${ this . id }, Name: ${ this . name }` ) ; const overrideOptions = this . design . getDesignOptions ( ) . override ; this . log . detail ( 'Match Configs' , this . configs ) ; this . timeStep = 0 ; if ( this . configs . storeErrorLogs ) { if ( ! existsSync ( this . configs . storeErrorDirectory ) ) { mkdirSync ( this . configs . storeErrorDirectory ) ; } const matchErrorLogDirectory = this . getMatchErrorLogDirectory ( ) ; if ( ! existsSync ( matchErrorLogDirectory ) ) { mkdirSync ( matchErrorLogDirectory ) ; } } this . matchEngine . killOffSignal = false ; const retrieveBotFilePromises = [ ] ; const retrieveBotFileIndexes = [ ] ; if ( this . dimension . hasStorage ( ) ) { this . agentFiles . forEach ( ( agentFile , index ) => { if ( agentFile . botkey && agentFile . file ) { let useCachedBotFile = false ; if ( this . configs . agentSpecificOptions [ index ] && this . configs . agentSpecificOptions [ index ] . useCachedBotFile ) { useCachedBotFile = true ; } retrieveBotFilePromises . push ( this . retrieveBot ( agentFile . botkey , agentFile . file , useCachedBotFile ) ) ; retrieveBotFileIndexes . push ( index ) ; } } ) ; } const retrievedBotFiles = await Promise . all ( retrieveBotFilePromises ) ; retrieveBotFileIndexes . forEach ( ( val , index ) => { if ( ! ( typeof this . agentFiles [ val ] === 'string' ) ) { this . agentFiles [ val ] . file = retrievedBotFiles [ index ] ; this . nonLocalFiles . push ( path . dirname ( retrievedBotFiles [ index ] ) ) ; } } ) ; this . agents = Agent . generateAgents ( this . agentFiles , this . configs . agentOptions , this . configs . languageSpecificAgentOptions ) ; this . agents . forEach ( ( agent ) => { this . idToAgentsMap . set ( agent . id , agent ) ; if ( agent . tournamentID !== null ) { this . mapAgentIDtoTournamentID . set ( agent . id , agent . tournamentID ) ; } } ) ; if ( ! this . configs . detached ) { if ( overrideOptions . active ) { this . log . detail ( 'Match Arguments' , overrideOptions . arguments ) ; await this . matchEngine . initializeCustom ( ) ; } else { await this . matchEngine . initialize ( this . agents , this ) ; } } await this . design . initialize ( this ) ; this . matchStatus = Match . Status . READY ; return true ; } catch ( err ) { await this . handleLogFiles ( ) ; await this . killAndCleanUp ( ) ; if ( err instanceof AgentError ) { if ( err instanceof AgentCompileError || err instanceof AgentInstallError ) { this . agents [ err . agentID ] . status = Agent . Status . CRASHED ; } } throw err ; } } param botkey param file param useCached private async retrieveBot ( botkey , file , useCached ) { const dir = BOT_DIR + '/anon-' + genID ( 18 ) ; mkdirSync ( dir ) ; const zipFile = path . join ( dir , 'bot.zip' ) ; const actualZipFileLocation = await this . dimension . storagePlugin . download ( botkey , zipFile , useCached ) ; await extract ( actualZipFileLocation , { dir : dir , } ) ; return path . join ( dir , path . basename ( file ) ) ; } /**
     * Runs this match to completion. Sets this.results to match results and resolves with the match results when done
     */ public run ( ) { return new Promise ( async ( resolve , reject ) => { try { this . runReject = reject ; let status ; this . matchStatus = Match . Status . RUNNING ; const overrideOptions = this . design . getDesignOptions ( ) . override ; if ( overrideOptions . active ) { this . log . system ( 'Running custom' ) ; await this . matchEngine . runCustom ( this ) ; this . results = await this . getResults ( ) ; if ( overrideOptions . resultHandler ) { this . results = overrideOptions . resultHandler ( this . results ) ; } } else { do { status = await this . next ( ) ; } while ( status != Match . Status . FINISHED ) ; this . agents . forEach ( ( agent ) => { agent . _clearTimer ( ) ; } ) ; this . results = await this . getResults ( ) ; } await this . killAndCleanUp ( ) ; if ( this . results . replayFile ) { if ( existsSync ( this . results . replayFile ) ) { if ( ! statSync ( this . results . replayFile ) . isDirectory ( ) ) { if ( this . configs . storeReplay ) { this . replayFile = this . results . replayFile ; if ( this . dimension . hasStorage ( ) ) { const fileName = path . basename ( this . results . replayFile ) ; const key = await this . dimension . storagePlugin . upload ( this . results . replayFile , `${ path . join ( this . configs . storeReplayDirectory , fileName ) }` ) ; this . replayFileKey = key ; removeFile ( this . replayFile ) ; } } else { removeFile ( this . results . replayFile ) ; } } else { reject ( new MatchReplayFileError ( `Replay file provided ${ this . results . replayFile } is not a file` ) ) ; } } else { reject ( new MatchReplayFileError ( `Replay file provided ${ this . results . replayFile } does not exist` ) ) ; } } await this . handleLogFiles ( ) ; this . finishDate = new Date ( ) ; resolve ( this . results ) ; } catch ( error ) { reject ( error ) ; } } ) ; } /**
     * Handles log files and stores / uploads / deletes them as necessary
     */ private async handleLogFiles ( ) { const uploadLogPromises = [ ] ; const fileLogsToRemove = [ ] ; if ( this . configs . storeErrorLogs ) { for ( const agent of this . agents ) { const filepath = path . join ( this . getMatchErrorLogDirectory ( ) , agent . getAgentErrorLogFilename ( ) ) ; if ( existsSync ( filepath ) ) { if ( agent . _logsize === 0 ) { fileLogsToRemove . push ( filepath ) ; } else if ( this . dimension . hasStorage ( ) ) { const uploadKeyPromise = this . dimension . storagePlugin . upload ( filepath , filepath ) . then ( ( key ) => { return { key : key , agentID : agent . id } ; } ) ; uploadLogPromises . push ( uploadKeyPromise ) ; fileLogsToRemove . push ( filepath ) ; } } else { this . log . error ( `Agent ${ this . id } log file at ${ filepath } does not exist` ) ; } } } const logkeys = await Promise . all ( uploadLogPromises ) ; logkeys . forEach ( ( val ) => { this . idToAgentsMap . get ( val . agentID ) . logkey = val . key ; } ) ; if ( fileLogsToRemove . length === this . agents . length ) { removeDirectory ( this . getMatchErrorLogDirectory ( ) ) ; } else { fileLogsToRemove . forEach ( ( logPath ) => { removeFile ( logPath ) ; } ) ; } } /**
     * Step forward the match by one timestep by sending commands individually.
     */ public async step ( commands ) { const engineOptions = this . matchEngine . getEngineOptions ( ) ; if ( engineOptions . commandStreamType === COMMAND_STREAM_TYPE . SEQUENTIAL ) { const status = ( await this . design . update ( this , commands ) ) ?? Match . Status . RUNNING ; return status ; } else { throw new NotSupportedError ( 'Only sequential streaming is allowed' ) ; } } /**
     * Next function. Moves match forward by one timestep. Resolves with the match status
     * This function should always used to advance forward a match unless a custom design is provided
     *
     * Gathers commands from agents via the  MatchEngine *
     * Should not be called by user
      public async next ( ) { const engineOptions = this . matchEngine . getEngineOptions ( ) ; if ( engineOptions . commandStreamType === COMMAND_STREAM_TYPE . SEQUENTIAL ) { if ( this . shouldStop == true ) { this . matchStatus = Match . Status . STOPPED ; this . matchEngine . stop ( this ) ; this . log . info ( 'Stopped match' ) ; this . resolveStopPromise ( ) ; await this . resumePromise ; this . matchEngine . resume ( this ) ; this . log . info ( 'Resumed match' ) ; this . shouldStop = false ; } this . agents . forEach ( ( agent ) => { agent . resume ( ) ; agent . _setupMove ( ) ; if ( engineOptions . timeout . active && ! agent . isTerminated ( ) ) { agent . _setTimeout ( ( ) => { agent . timeout ( ) ; } , engineOptions . timeout . max + MatchEngine . timeoutBuffer ) ; } } ) ; const commands = await this . matchEngine . getCommands ( this ) ; this . log . system ( `Retrieved ${ commands . length } commands` ) ; const status = await this . design . update ( this , commands ) ; if ( ! status ) { this . matchStatus = Match . Status . RUNNING ; } else { this . matchStatus = status ; } this . timeStep += 1 ; return status ; } else if ( engineOptions . commandStreamType === COMMAND_STREAM_TYPE . PARALLEL ) { throw new NotSupportedError ( 'PARALLEL command streaming has not been implemented yet' ) ; } } /**
     * Stops the match. For non-custom designs, stops at the next nearest timestep possible. Otherwise attempts to stop
     * the match using the  MatchEngine stopCustom function.
     *
     * Notes:
     * - If design uses a PARALLEL match engine, stopping behavior can be a little unpredictable
     * - If design uses a SEQUENTIAL match engine, a stop will result in ensuring all agents complete all their actions up
     *   to a coordinated stopping `timeStep`
      public stop ( ) { return new Promise ( ( resolve , reject ) => { if ( this . matchStatus != Match . Status . RUNNING ) { this . log . warn ( "You can't stop a match that is not running" ) ; reject ( new MatchWarn ( 'You can\t stop a match that is not running' ) ) ; return ; } if ( this . design . getDesignOptions ( ) . override . active ) { this . matchEngine . stopCustom ( this ) . then ( ( ) => { this . matchStatus = Match . Status . STOPPED ; resolve ( ) ; } ) . catch ( reject ) ; return ; } else { this . resolveStopPromise = resolve ; this . log . info ( 'Stopping match...' ) ; this . resumePromise = new Promise ( ( resolve ) => { this . resumeResolve = resolve ; } ) ; this . shouldStop = true ; } } ) ; } returns public resume ( ) { return new Promise ( ( resolve , reject ) => { if ( this . matchStatus != Match . Status . STOPPED ) { this . log . warn ( "You can't resume a match that is not stopped" ) ; reject ( new MatchWarn ( "You can't resume a match that is not stopped" ) ) ; return ; } this . log . info ( 'Resuming match...' ) ; if ( this . design . getDesignOptions ( ) . override . active ) { this . matchEngine . resumeCustom ( this ) . then ( ( ) => { this . matchStatus = Match . Status . RUNNING ; resolve ( ) ; } ) . catch ( reject ) ; } else { this . matchStatus = Match . Status . RUNNING ; this . resumeResolve ( ) ; resolve ( ) ; } } ) ; } /**
     * Stop all agents through the match engine and clean up any other files and processes
     *
     * Used by custom and dimensions based designs
     */ private async killAndCleanUp ( ) { await this . matchEngine . killAndClean ( this ) ; const removeNonLocalFilesPromises = [ ] ; this . nonLocalFiles . forEach ( ( nonLocalFile ) => { removeNonLocalFilesPromises . push ( removeDirectory ( nonLocalFile ) ) ; } ) ; await Promise . all ( removeNonLocalFilesPromises ) ; } /**
     * Terminate an  Agent , kill the process. Note, the agent is still stored in the Match, but you can't send or
     * receive messages from it anymore
     *
     *  param agent param reason public async kill ( agent , reason ? ) { if ( agent instanceof Agent ) { this . matchEngine . kill ( agent , reason ) ; } else { this . matchEngine . kill ( this . idToAgentsMap . get ( agent ) , reason ) ; } } /**
     * Retrieve results through delegating the task to  Design . getResults public async getResults ( ) { return await this . design . getResults ( this ) ; } param message returns public sendAll ( message ) { return new Promise ( ( resolve ) => { const sendPromises = [ ] ; this . agents . forEach ( ( agent ) => { sendPromises . push ( this . send ( message , agent ) ) ; } ) ; Promise . all ( sendPromises ) . then ( ( sendStatus ) => { resolve ( sendStatus . every ( ( v ) => v === true ) ) ; } ) ; } ) ; } /**
     * Functional method for sending a message string to a particular  Agent . Returns a promise that resolves true
     * if succesfully sent. Returns false if could not send message, meaning agent was also killed.
     *  param message param receiver - receiver of message can be specified by the  Agent or it's  Agent . ID (a number)
      public async send ( message , receiver ) { if ( receiver instanceof Agent ) { try { await this . matchEngine . send ( this , message , receiver . id ) ; } catch ( err ) { this . log . error ( err ) ; await this . kill ( receiver , 'could not send message anymore' ) ; return false ; } } else { try { await this . matchEngine . send ( this , message , receiver ) ; } catch ( err ) { this . log . error ( err ) ; await this . kill ( receiver , 'could not send message anymore' ) ; return false ; } } return true ; } /**
     * Throw an  FatalError ,  MatchError , or  MatchWarn within the Match. Indicates that the
     *  Agent with id agentID caused this error/warning.
     *
     * Throwing MatchWarn will just log a warning level message and throwing a MatchError will just log it as an error
     * level message.
     *
     * Throwing FatalError will cause the match to automatically be destroyed. This is highly not recommended and it is
     * suggested to have some internal logic to handle moments when the match cannot continue.
     *
     *
     * Examples are misuse of an existing command or using incorrect commands or sending too many commands
     *  param agentID param error public async throw ( agentID , error ) { const agent = this . idToAgentsMap . get ( agentID ) ; if ( error instanceof FatalError ) { await this . destroy ( ) ; const msg = `FatalError: ${ agent . name } | ${ error . message }` ; this . log . error ( msg ) ; if ( this . configs . storeMatchErrorLogs ) agent . writeToErrorLog ( msg ) ; } else if ( error instanceof MatchWarn ) { const msg = `ID: ${ agentID }, ${ this . idToAgentsMap . get ( agentID ) . name } | ${ error . message }` ; this . log . warn ( msg ) ; if ( this . configs . storeMatchErrorLogs ) agent . writeToErrorLog ( msg ) ; } else if ( error instanceof MatchError ) { const msg = `ID: ${ agentID }, ${ this . idToAgentsMap . get ( agentID ) . name } | ${ error . message }` ; this . log . error ( msg ) ; if ( this . configs . storeMatchErrorLogs ) agent . writeToErrorLog ( msg ) ; } else { this . log . error ( 'User tried throwing an error of type other than FatalError, MatchWarn, or MatchError' ) ; } } /**
     * Destroys this match and makes sure to remove any leftover processes
     */ public async destroy ( ) { if ( this . runReject ) this . runReject ( new MatchDestroyedError ( 'Match was destroyed' ) ) ; await this . killAndCleanUp ( ) ; await this . matchEngine . killAndCleanCustom ( this ) ; } /**
     * Generates a 12 character nanoID string for identifying matches
     */ public static genMatchID ( ) { return genID ( 12 ) ; } public getMatchErrorLogDirectory ( ) { return path . join ( this . configs . storeErrorDirectory , `match_${ this . id }` ) ; } } export namespace Match { /**
     * Match Configurations. Has 5 specified fields. All other fields are up to user discretion
     */ export interface Configs { /**
         * Name of the match
         */ name : string ; see Logger loggingLevel : Logger . LEVEL ; /**
         * The engine options to use in this match.
         */ engineOptions : DeepPartial < EngineOptions > ; default secureMode : boolean ; /**
         * Default Agent options to use for all agents in a match. Commonly used for setting resource use boundaries
         */ agentOptions : DeepPartial < Agent . Options > ; default languageSpecificAgentOptions : Agent . LanguageSpecificOptions ; default agentSpecificOptions : Array < DeepPartial < Agent . Options > > ; default storeReplay : boolean ; /**
         * Used only when a  Storage plugin is used. Indicates the directory to use to store onto the storage.
         * (Typically some path in the bucket).
         *
         *  default storeReplayDirectory : string ; /**
         * Whether to store error output for each  Match default storeErrorLogs : boolean ; default storeMatchErrorLogs : boolean ; /**
         * Directory to store error logs locally. When a  Storage plugin is used, this indicates the path in the
         * bucket to store the log in, and removes the local copy.
         *
         *  default storeErrorDirectory : string ; /**
         * Whether to run in detached mode. When in detached mode (true), the match can initialize within dimensions using the  Design but will now
         * instead update step by step
         *
         *  default detached : boolean ; [ key ] : any ; } export enum Status { /** Match was created with new but initialize was not called */ UNINITIALIZED = 'uninitialized' , /**
         * If the match has been initialized and checks have been passed, the match is ready to run using  Match . run READY = 'ready' , /**
         * If the match is running at the moment
         */ RUNNING = 'running' , /**
         * If the match is stopped
         */ STOPPED = 'stopped' , /**
         * If the match is completed
         */ FINISHED = 'finished' , /**
         * If fatal error occurs in Match, appears when match stops itself
         */ ERROR = 'error' } }