import UNITS_BY_NAME from "../constants/units_by_name" ; import TRAINED_BY from "../constants/trained_by" ; import UPGRADES_BY_NAME from "../constants/upgrade_by_name" ; import RESEARCHED_BY from "../constants/researched_by" ; import { BO_ITEMS , supplyUnitNameByRace } from "../constants/bo_items" ; import { incomeMinerals , incomeVespene } from "./income" ; import { cloneDeep , minBy , find , remove } from "lodash" ; import Unit from "./unit" ; import Event from "./event" ; import Task from "./task" ; import executeAction from "./execute_action" ; import { defaultSettings , defaultOptimizeSettings } from "../constants/helper" ; import { IBuildOrderElement , ISettingsElement , ICost , IAllRaces , IResourceHistory , } from "../constants/interfaces" ; /** Logic of this file:
Each frame
    Calculate and add income
    Update the state of each unit:
        Update energy available
        Update progress (if they are doing something)
    Check if the next build order item can be executed

Each build order index increment
    Add the current state to snapshots for cached view
*/ type IError = { message : string ; requirements : IBuildOrderElement [ ] ; neededEffort : number ; } ; let eventId = 0 ; let mineralIncomeCache = { } ; let vespeneIncomeCache = { } ; const workerTypes = new Set ( [ "SCV" , "Probe" , "Drone" ] ) ; class GameLogic { race : IAllRaces ; bo : Array < IBuildOrderElement > ; boIndex : number ; minerals : number ; vespene : number ; supplyUsed : number ; supplyLeft : number ; supplyCap : number ; raceSpecificResource : number ; resourceHistory : IResourceHistory ; units : Set < Unit > ; idleUnits : Set < Unit > ; busyUnits : Set < Unit > ; workersMinerals : number ; workersVespene : number ; workersScouting : number ; muleCount : number ; upgrades : Set < string > ; baseCount : number ; gasCount : number ; freeTechlabs : number ; freeReactors : number ; frame : number ; waitTime : number ; eventLog : Array < Event > ; unitsCountArray : Array < { [ name ] : number ; } > ; errorMessage : string ; requirements ? : IBuildOrderElement [ ] ; settings : { [ name ] : number | string ; } ; customSettings : Array < ISettingsElement > ; optimizeSettings : { [ name ] : number | string ; } ; customOptimizeSettings : Array < ISettingsElement > ; constructor ( race = "terran" , bo = [ ] , customSettings = [ ] , customOptimizeSettings = [ ] ) { this . race = race ; this . bo = bo ; this . boIndex = 0 ; this . minerals = 50 ; this . vespene = 0 ; this . supplyUsed = 12 ; this . supplyLeft = this . race === "zerg" ? 2 : 3 ; this . supplyCap = this . race === "zerg" ? 14 : 15 ; this . raceSpecificResource = this . race === "protoss" ? 1 : this . race === "terran" ? 0 : 3 ; this . resourceHistory = { minerals : [ this . minerals ] , vespene : [ this . vespene ] , supplyLeft : [ this . supplyLeft ] , raceSpecificResource : [ this . raceSpecificResource ] , } ; this . units = new Set ( ) ; this . idleUnits = new Set ( ) ; this . busyUnits = new Set ( ) ; this . workersMinerals = 12 ; this . workersVespene = 0 ; this . workersScouting = 0 ; this . muleCount = 0 ; this . upgrades = new Set ( ) ; this . baseCount = 1 ; this . gasCount = 0 ; this . freeTechlabs = 0 ; this . freeReactors = 0 ; this . frame = 0 ; this . waitTime = 0 ; this . eventLog = [ ] ; this . unitsCountArray = [ ] ; this . errorMessage = "" ; this . requirements = undefined ; eventId = - 1 ; mineralIncomeCache = { } ; vespeneIncomeCache = { } ; this . customSettings = customSettings ; this . settings = { } ; this . loadSettings ( defaultSettings ) ; this . loadSettings ( customSettings ) ; this . customOptimizeSettings = customOptimizeSettings ; this . optimizeSettings = { } ; this . loadOptimizeSettings ( defaultOptimizeSettings ) ; this . loadOptimizeSettings ( customOptimizeSettings ) ; } /**
     * Prepares default properties for this.setStart()
     */ reset ( ) { this . boIndex = 0 ; this . minerals = 50 ; this . vespene = 0 ; this . supplyUsed = 12 ; this . supplyLeft = this . race === "zerg" ? 2 : 3 ; this . supplyCap = this . race === "zerg" ? 14 : 15 ; this . raceSpecificResource = this . race === "protoss" ? 1 : this . race === "terran" ? 0 : 3 ; this . resourceHistory = { minerals : [ this . minerals ] , vespene : [ this . vespene ] , supplyLeft : [ this . supplyLeft ] , raceSpecificResource : [ this . raceSpecificResource ] , } ; this . units = new Set ( ) ; this . idleUnits = new Set ( ) ; this . busyUnits = new Set ( ) ; this . workersMinerals = 0 ; this . workersVespene = 0 ; this . workersScouting = 0 ; this . muleCount = 0 ; this . upgrades = new Set ( ) ; this . baseCount = 1 ; this . gasCount = 0 ; this . freeTechlabs = 0 ; this . freeReactors = 0 ; this . frame = 0 ; this . waitTime = 0 ; this . eventLog = [ ] ; this . unitsCountArray = [ ] ; eventId = - 1 ; mineralIncomeCache = { } ; vespeneIncomeCache = { } ; } param { Object } customSettings loadSettings ( customSettings ) { for ( const item of customSettings ) { this . settings [ item . variableName ] = item . v ; } } loadOptimizeSettings ( customSettings ) { for ( const item of customSettings ) { this . optimizeSettings [ item . variableName ] = item . v ; } } exportSettings ( ) { const settingsObject = cloneDeep ( defaultSettings ) ; settingsObject . forEach ( ( item ) => { item . v = this . settings [ item . variableName ] ; } ) ; return settingsObject ; } exportOptimizeSettings ( ) { const optimizeSettingsObject = cloneDeep ( defaultOptimizeSettings ) ; optimizeSettingsObject . forEach ( ( item ) => { item . v = this . optimizeSettings [ item . variableName ] ; } ) ; return optimizeSettingsObject ; } /**
     *
     */ getEventId ( ) { eventId += 1 ; return eventId ; } /**
     * Sets the start conditions: spawns all start structures, starting resources and values will be set in this.reset()
     */ setStart ( ) { this . reset ( ) ; let townhallName = "" ; let workerName = "" ; if ( this . race === "terran" ) { townhallName = "CommandCenter" ; workerName = "SCV" ; } if ( this . race === "protoss" ) { townhallName = "Nexus" ; workerName = "Probe" ; } if ( this . race === "zerg" ) { townhallName = "Hatchery" ; workerName = "Drone" ; this . units . add ( new Unit ( "Overlord" ) ) ; } const townhall = new Unit ( townhallName ) ; townhall . energy = 50 ; if ( this . race === "zerg" ) { townhall . larvaCount = 3 ; } this . units . add ( townhall ) ; this . idleUnits . add ( townhall ) ; for ( let i = 0 ; i < 12 ; i ++ ) { const unit = new Unit ( workerName ) ; const workerStartDelayTask = new Task ( 22.4 * + this . settings . workerStartDelay , this . frame , this . supplyUsed ) ; workerStartDelayTask . addMineralWorker = true ; unit . addTask ( this , workerStartDelayTask ) ; this . units . add ( unit ) ; } this . unitsCountArray . push ( this . updateUnitsCount ( ) ) ; } /**
     * Runs the simulation until a limit is reached (20 minutes)
     * or until the end of build order is reached and no unit is busy (= no unit has a task)
     * or if no unit was busy for a long time, then it is assumed that the build order cannot be executed completely and must be invalid
     */ runUntilEnd ( ) { while ( this . frame < 22.4 * 20 * 60 ) { this . runFrame ( ) ; this . frame += 1 ; if ( this . boIndex >= this . bo . length && this . busyUnits . size === 0 ) { break ; } if ( this . busyUnits . size === 0 ) { this . settings . idleTime = + this . settings . idleTime + 1 ; if ( this . settings . idleTime >= + this . settings . idleLimit * 22.4 ) { break ; } } else { this . settings . idleTime = 0 ; } } this . eventLog . sort ( ( a , b ) => { if ( a . id < b . id ) { return - 1 ; } if ( a . id > b . id ) { return 1 ; } return 0 ; } ) ; } /**
     * Executes one frame of the simulation
     * Adds income
     * Steps each unit by 1 frame (task progress, energy generation etc.)
     * Tries to execute next build order elements until it is no longer possible
     */ runFrame ( ) { this . raceSpecificResource = 0 ; this . addIncome ( ) ; this . updateUnitsProgress ( ) ; let endOfActions = false ; while ( ! endOfActions && this . boIndex < this . bo . length ) { endOfActions = true ; const boElement = this . bo [ this . boIndex ] ; if ( [ "worker" , "unit" ] . includes ( boElement . type ) ) { const trained = this . trainUnit ( boElement ) ; if ( trained ) { endOfActions = false ; } } else if ( boElement . type === "structure" ) { const built = this . trainUnit ( boElement ) ; if ( built ) { endOfActions = false ; } } else if ( boElement . type === "upgrade" ) { const researched = this . researchUpgrade ( boElement ) ; if ( researched ) { endOfActions = false ; } } else if ( boElement . type === "action" ) { const executed = executeAction ( this , boElement ) ; if ( executed ) { endOfActions = false ; } } if ( ! endOfActions ) { this . boIndex += 1 ; this . errorMessage = "" ; this . requirements = undefined ; this . unitsCountArray . push ( this . updateUnitsCount ( ) ) ; } } this . resourceHistory . minerals . push ( this . minerals ) ; this . resourceHistory . vespene . push ( this . vespene ) ; this . resourceHistory . supplyLeft . push ( this . supplyLeft ) ; this . resourceHistory . raceSpecificResource . push ( this . raceSpecificResource ) ; } /**
     * Updates the statistics of available actions, how many units and structures we have, and what upgrades are researched
     */ updateUnitsCount ( ) { const unitsCount = { } ; const incrementUnitName = ( item , amount = 1 ) => { if ( ! unitsCount [ item ] ) { unitsCount [ item ] = amount ; } else { unitsCount [ item ] += amount ; } } ; let htCount = 0 ; let dtCount = 0 ; this . units . forEach ( ( unit , _index ) => { if ( ! [ "Drone" , "HighTemplar" , "DarkTemplar" ] . includes ( unit . name ) || unit . tasks . length === 0 || ! unit . tasks [ unit . tasks . length - 1 ] . morphToUnit ) { incrementUnitName ( unit . name ) ; } if ( unit . hasTechlab ) { incrementUnitName ( `${ unit . name }Techlab` ) ; } else if ( unit . hasReactor ) { incrementUnitName ( `${ unit . name }Reactor` ) ; } if ( unit . larvaCount > 0 ) { incrementUnitName ( "Larva" , unit . larvaCount ) ; } if ( unit . isMiningMinerals ( ) ) { incrementUnitName ( "worker_to_mins" ) ; } if ( unit . isMiningGas ) { incrementUnitName ( "worker_to_gas" ) ; } if ( unit . isScouting ) { incrementUnitName ( "worker_to_scout" ) ; } if ( unit . name === "Nexus" ) { const amount = Math . floor ( unit . energy / 50 ) ; incrementUnitName ( "chronoboost_busy_nexus" , amount ) ; incrementUnitName ( "chronoboost_busy_gateway" , amount ) ; incrementUnitName ( "chronoboost_busy_warpgate" , amount ) ; incrementUnitName ( "chronoboost_busy_cybercore" , amount ) ; incrementUnitName ( "chronoboost_busy_forge" , amount ) ; incrementUnitName ( "chronoboost_busy_robo" , amount ) ; incrementUnitName ( "chronoboost_busy_stargate" , amount ) ; incrementUnitName ( "chronoboost_busy_twilight" , amount ) ; incrementUnitName ( "chronoboost_busy_dark_shrine" , amount ) ; incrementUnitName ( "chronoboost_busy_templar_archive" , amount ) ; incrementUnitName ( "chronoboost_busy_fleet_beacon" , amount ) ; incrementUnitName ( "chronoboost_busy_robotics_bay" , amount ) ; } if ( unit . name === "Gateway" && this . upgrades . has ( "WarpGate" ) ) { incrementUnitName ( "convert_gateway_to_warpgate" ) ; } if ( unit . name === "WarpGate" ) { incrementUnitName ( "convert_warpgate_to_gateway" ) ; } if ( unit . name === "HighTemplar" ) { htCount += 1 ; } if ( unit . name === "DarkTemplar" ) { dtCount += 1 ; } if ( unit . name === "OrbitalCommand" ) { const amount = Math . floor ( unit . energy / 50 ) ; incrementUnitName ( "call_down_mule" , amount ) ; incrementUnitName ( "call_down_supply" , amount ) ; } if ( this . freeTechlabs > 0 ) { if ( unit . name === "Barracks" && ! unit . hasAddon ( ) ) { incrementUnitName ( "attach_barracks_to_free_techlab" ) ; } if ( unit . name === "Factory" && ! unit . hasAddon ( ) ) { incrementUnitName ( "attach_factory_to_free_techlab" ) ; } if ( unit . name === "Starport" && ! unit . hasAddon ( ) ) { incrementUnitName ( "attach_starport_to_free_techlab" ) ; } } if ( this . freeReactors > 0 ) { if ( unit . name === "Barracks" && ! unit . hasAddon ( ) ) { incrementUnitName ( "attach_barracks_to_free_reactor" ) ; } if ( unit . name === "Factory" && ! unit . hasAddon ( ) ) { incrementUnitName ( "attach_factory_to_free_reactor" ) ; } if ( unit . name === "Starport" && ! unit . hasAddon ( ) ) { incrementUnitName ( "attach_starport_to_free_reactor" ) ; } } if ( unit . name === "Barracks" && unit . hasTechlab ) { incrementUnitName ( "dettach_barracks_from_techlab" ) ; } if ( unit . name === "Barracks" && unit . hasReactor ) { incrementUnitName ( "dettach_barracks_from_reactor" ) ; } if ( unit . name === "Factory" && unit . hasTechlab ) { incrementUnitName ( "dettach_factory_from_techlab" ) ; } if ( unit . name === "Factory" && unit . hasReactor ) { incrementUnitName ( "dettach_factory_from_reactor" ) ; } if ( unit . name === "Starport" && unit . hasTechlab ) { incrementUnitName ( "dettach_starport_from_techlab" ) ; } if ( unit . name === "Starport" && unit . hasReactor ) { incrementUnitName ( "dettach_starport_from_reactor" ) ; } if ( unit . name === "Bunker" ) { incrementUnitName ( "salvage_bunker" ) ; } if ( unit . name === "Queen" ) { const amount = Math . floor ( unit . energy / 25 ) ; incrementUnitName ( "inject" , amount ) ; incrementUnitName ( "creep_tumor" , amount ) ; } } ) ; incrementUnitName ( "morph_archon_from_dt_dt" , Math . floor ( dtCount / 2 ) ) ; incrementUnitName ( "morph_archon_from_ht_ht" , Math . floor ( htCount / 2 ) ) ; incrementUnitName ( "morph_archon_from_ht_dt" , Math . min ( htCount , dtCount ) ) ; this . upgrades . forEach ( ( upgrade , _index ) => { incrementUnitName ( upgrade ) ; } ) ; incrementUnitName ( "minerals" , this . minerals ) ; incrementUnitName ( "vespene" , this . vespene ) ; incrementUnitName ( "supplyused" , this . supplyUsed ) ; incrementUnitName ( "supplycap" , this . supplyCap ) ; incrementUnitName ( "frame" , this . frame ) ; return unitsCount ; } /**
     * Checks if what an item requires is present or not
     */ addRequirements ( itemName , requires ) { this . requirements = this . requirements || [ ] ; const itemPresence = { } ; for ( const item of this . units ) { let itemName = item . name ; if ( item . hasTechlab ) { itemName += "TechLab" ; } if ( item . hasReactor ) { itemName += "Reactor" ; } itemPresence [ itemName ] = true ; } for ( const upgradeName of this . upgrades ) { itemPresence [ upgradeName ] = true ; } const errorList = [ ] ; for ( const requirementList of requires ) { const error = { message : "" , requirements : [ ] , neededEffort : 0 , } ; errorList . push ( error ) ; for ( const requiredItem of requirementList ) { if ( ! itemPresence [ requiredItem ] ) { error . message = `Required ${ requiredItem } for ${ itemName } could not be found.` ; error . requirements . push ( BO_ITEMS [ requiredItem ] ) ; error . neededEffort += 1 / requirementList . length ; } } } const leastEffortError = minBy ( errorList , "neededEffort" ) ; if ( leastEffortError && leastEffortError . neededEffort ) { this . errorMessage = leastEffortError . message ; this . requirements . push ( ... leastEffortError . requirements ) ; return false ; } return true ; } /**
     * The simulation tries to train a unit, builds a structure or morphs a unit
     */ trainUnit ( unit ) { console . assert ( unit . name , JSON . stringify ( unit , null , 4 ) ) ; console . assert ( unit . type , JSON . stringify ( unit , null , 4 ) ) ; const trainInfo = TRAINED_BY [ unit . name ] ; this . requirements = [ ] ; let morphCondition = trainInfo . isMorph || trainInfo . consumesUnit ; console . assert ( trainInfo , unit . name ) ; if ( trainInfo . requires . length ) { const requirements = this . addRequirements ( unit . name , trainInfo . requires ) ; if ( ! requirements ) { return false ; } } const cost = this . getCost ( unit . name ) ; if ( ! this . _canAfford ( cost ) && ! morphCondition ) { this . setCostErrorMessage ( cost , unit . name ) ; return false ; } for ( const trainerUnit of this . idleUnits ) { if ( ! trainerUnit . isIdle ( ) ) { continue ; } if ( ( unit . name . includes ( "TechLab" ) || unit . name . includes ( "Reactor" ) ) && trainerUnit . hasAddon ( ) ) { this . errorMessage = `Could not find structure without addon to build '${ unit . name }'.` ; continue ; } const trainerCanTrainThisUnit = trainInfo . trainedBy . has ( trainerUnit . name ) && ( ! trainInfo . requiresTechlab || trainerUnit . hasTechlab ) ; const trainerCanTrainThroughReactor = trainInfo . trainedBy . has ( trainerUnit . name ) && ! trainInfo . requiresTechlab && trainerUnit . hasReactor && trainerUnit . addonTasks . length === 0 ; const trainerCanTrainThroughLarva = ( trainInfo . trainedBy . has ( "Larva" ) && trainerUnit . larvaCount > 0 ) || ( unit . type === "structure" && trainerUnit . name === "Probe" ) ; morphCondition = morphCondition && ! trainerCanTrainThroughLarva ; if ( ! trainerCanTrainThroughLarva && ! trainerCanTrainThroughReactor ) { if ( trainerUnit . tasks . length > 0 ) { continue ; } } if ( ! trainerCanTrainThisUnit && ! trainerCanTrainThroughReactor && ! trainerCanTrainThroughLarva ) { this . errorMessage = `Could not find unit to produce '${ unit . name }'.` ; if ( trainInfo . consumesUnit ) { if ( unit . type === "structure" ) { this . requirements = [ { name : "Drone" , type : "worker" , } , ] ; } else { this . errorMessage += ` Didn't know which requirement to insert here for ${ unit . name }` ; } } continue ; } if ( unit . name === "Zergling" ) { cost . supply = 1 ; cost . minerals = 50 ; } else if ( morphCondition ) { const costTrainer = this . getCost ( trainerUnit . name ) ; cost . minerals -= costTrainer . minerals ; cost . vespene -= costTrainer . vespene ; if ( costTrainer . supply > 0 ) { cost . supply -= costTrainer . supply ; } } if ( ! this . _canAfford ( cost ) ) { this . setCostErrorMessage ( cost , unit . name ) ; return false ; } let buildTime = this . getTime ( unit . name ) ; let buildStartDelay = 0 ; if ( workerTypes . has ( trainerUnit . name ) ) { this . workersMinerals -= 1 ; const workerMovingToConstructionSite = new Task ( + this . settings . workerBuildDelay * 22.4 , this . frame , this . supplyUsed ) ; trainerUnit . addTask ( this , workerMovingToConstructionSite ) ; buildStartDelay = + this . settings . workerBuildDelay * 22.4 ; if ( trainerUnit . name === "Probe" ) { buildTime += buildStartDelay ; } } const taskId = this . getEventId ( ) ; const newTask = new Task ( buildTime , this . frame + buildStartDelay , this . supplyUsed , taskId ) ; newTask . morphToUnit = morphCondition || trainInfo . consumesUnit ? unit . name : null ; if ( newTask . morphToUnit === null ) { if ( unit . type === "worker" ) { newTask . newWorker = unit . name ; } else if ( unit . type === "unit" ) { newTask . newUnit = unit . name ; } else if ( unit . type === "structure" ) { newTask . newStructure = unit . name ; } } if ( trainerUnit . name === "WarpGate" ) { newTask . totalFramesRequired = 3.6 * 22.4 ; trainerUnit . addTask ( this , newTask , trainerCanTrainThroughReactor , true ) ; trainerUnit . addTask ( this , new Task ( this . warpgateRecoverTime ( unit . name ) , this . frame , this . supplyUsed , taskId ) , trainerCanTrainThroughReactor , trainerCanTrainThroughLarva ) ; } else { trainerUnit . addTask ( this , newTask , trainerCanTrainThroughReactor , trainerCanTrainThroughLarva ) ; } if ( [ "Probe" , "SCV" ] . includes ( trainerUnit . name ) ) { const workerReturnToMinerals = new Task ( + this . settings . workerReturnDelay * 22.4 , this . frame , this . supplyUsed ) ; workerReturnToMinerals . addMineralWorker = true ; trainerUnit . addTask ( this , workerReturnToMinerals ) ; } if ( trainerCanTrainThroughLarva ) { trainerUnit . larvaCount -= 1 ; } this . minerals -= cost . minerals ; this . vespene -= cost . vespene ; if ( cost . supply > 0 ) { this . supplyUsed += cost . supply ; this . supplyLeft -= cost . supply ; } if ( trainerUnit . name === "Drone" ) { this . supplyUsed -= 1 ; this . supplyLeft += 1 ; } return true ; } return false ; } /**
     * The simulation tries to research an upgrade
     * Nearly the same as trainUnit(unit)
     */ researchUpgrade ( upgrade ) { this . requirements = [ ] ; console . assert ( upgrade . name , JSON . stringify ( upgrade , null , 4 ) ) ; console . assert ( upgrade . type , JSON . stringify ( upgrade , null , 4 ) ) ; const researchInfo = RESEARCHED_BY [ upgrade . name ] ; const requiredStructure = researchInfo . requiredStructure ; let requiredStructureMet = requiredStructure === null ? true : false ; if ( ! requiredStructureMet ) { for ( const structure of this . units ) { if ( structure . name === requiredStructure ) { requiredStructureMet = true ; break ; } } if ( ! requiredStructureMet && ! this . addRequirements ( upgrade . name , researchInfo . requires ) ) { return false ; } } const requiredUpgrade = researchInfo . requiredUpgrade ; let requiredUpgradeMet = requiredUpgrade === null ? true : false ; if ( requiredUpgrade && ! requiredUpgradeMet ) { if ( this . upgrades . has ( requiredUpgrade ) ) { requiredUpgradeMet = true ; } if ( ! requiredUpgradeMet ) { this . errorMessage = `Required upgrade '${ requiredUpgrade }' to research upgrade '${ upgrade . name }' could not be found.` ; this . requirements . push ( { name : requiredUpgrade , type : "upgrade" , } ) ; return false ; } } if ( ! this . addRequirements ( upgrade . name , researchInfo . requires ) ) { return false ; } const cost = this . getCost ( upgrade . name , true ) ; if ( ! this . _canAfford ( cost ) ) { this . setCostErrorMessage ( cost , upgrade . name ) ; return false ; } for ( const researcherStructure of this . idleUnits ) { const structureCanResearchUpgrade = researchInfo . researchedBy . has ( researcherStructure . name ) ; const canBeResearchedByAddon = researcherStructure . hasTechlab && researchInfo . researchedBy . has ( `${ researcherStructure . name }TechLab` ) ; if ( ! structureCanResearchUpgrade && ! canBeResearchedByAddon ) { continue ; } if ( structureCanResearchUpgrade && researcherStructure . tasks . length !== 0 ) { continue ; } if ( canBeResearchedByAddon && researcherStructure . addonTasks . length !== 0 ) { continue ; } const researchTime = this . getTime ( upgrade . name , true ) ; const newTask = new Task ( researchTime , this . frame , this . supplyUsed , this . getEventId ( ) ) ; newTask . newUpgrade = upgrade . name ; researcherStructure . addTask ( this , newTask , canBeResearchedByAddon ) ; const cost = this . getCost ( upgrade . name , true ) ; this . minerals -= cost . minerals ; this . vespene -= cost . vespene ; return true ; } return false ; } /**
     * Updates the unit state and progress of all living units
     */ updateUnitsProgress ( ) { this . units . forEach ( ( unit ) => { unit . updateUnitState ( this ) ; } ) ; this . units . forEach ( ( unit ) => { const isAlive = unit . isAlive ( this . frame ) ; if ( ! isAlive ) { if ( unit . name === "MULE" ) { this . muleCount -= 1 ; } this . killUnit ( unit ) ; } } ) ; this . units . forEach ( ( unit ) => { unit . updateUnit ( this ) ; } ) ; } /**
     * Kills a unit and removes it from the game logic
     */ killUnit ( unit ) { this . units . delete ( unit ) ; this . idleUnits . delete ( unit ) ; this . busyUnits . delete ( unit ) ; } setCostErrorMessage ( cost , unitName ) { if ( cost . supply > this . supplyLeft ) { this . errorMessage = `Missing ${ Math . ceil ( cost . supply - this . supplyLeft ) } supply to produce '${ unitName }'.` ; this . requirements = [ supplyUnitNameByRace [ this . race ] as IBuildOrderElement ] ; } else if ( cost . vespene > this . vespene ) { this . errorMessage = `Unable to afford '${ unitName }', missing ${ Math . ceil ( cost . vespene - this . vespene ) } vespene.` ; if ( this . workersVespene === 0 ) { this . requirements = [ { name : "3worker_to_gas" , type : "action" , } , ] ; } } else if ( cost . minerals > this . minerals ) { this . errorMessage = `Unable to afford '${ unitName }', missing ${ Math . ceil ( cost . minerals - this . minerals ) } minerals.` ; } } increaseMaxSupply ( amount ) { const remainingTillMaxSupply = 200 - this . supplyCap ; const increaseAmount = Math . min ( amount , remainingTillMaxSupply ) ; this . supplyCap += increaseAmount ; this . supplyLeft += increaseAmount ; } /**
     * Gets the cost of a unit, structure, morph or upgrade
     */ getCost ( unitName , isUpgrade = false ) { if ( isUpgrade ) { console . assert ( UPGRADES_BY_NAME [ unitName ] , `${ unitName }` ) ; return { minerals : UPGRADES_BY_NAME [ unitName ] . cost . minerals , vespene : UPGRADES_BY_NAME [ unitName ] . cost . gas , supply : 0 , } ; } console . assert ( UNITS_BY_NAME [ unitName ] , `${ unitName }` ) ; return { minerals : UNITS_BY_NAME [ unitName ] . minerals , vespene : UNITS_BY_NAME [ unitName ] . gas , supply : UNITS_BY_NAME [ unitName ] . supply , } ; } /**
     * Gets build or research time of a unit, structure, morph or upgrade
     */ getTime ( unitName , isUpgrade = false ) { if ( isUpgrade ) { console . assert ( UPGRADES_BY_NAME [ unitName ] , `${ unitName }` ) ; return UPGRADES_BY_NAME [ unitName ] . cost . time ; } console . assert ( UNITS_BY_NAME [ unitName ] , `${ unitName }` ) ; return UNITS_BY_NAME [ unitName ] . time ; } /**
     * Recover time of warp gates when creating a specific unit, how long the warp gate will be on cooldown after warping in a specific unit
     */ warpgateRecoverTime ( unitName ) { const recoverTimes = { Zealot : 20 * 22.4 , Adept : 20 * 22.4 , Stalker : 23 * 22.4 , Sentry : 23 * 22.4 , DarkTemplar : 32 * 22.4 , HighTemplar : 32 * 22.4 , } ; return recoverTimes [ unitName ] ; } /**
     * Calculates and caches the income based on how many workers (and mules), bases and gas structures we have
     */ addIncome ( ) { let minerals = mineralIncomeCache [ [ this . workersMinerals , this . baseCount , this . muleCount ] ] ; if ( minerals === undefined ) { minerals = incomeMinerals ( this . workersMinerals , this . baseCount , this . muleCount ) / + this . settings . incomeFactor ; mineralIncomeCache [ [ this . workersMinerals , this . baseCount , this . muleCount ] ] = minerals ; } let vespene = vespeneIncomeCache [ [ this . workersVespene , this . gasCount ] ] ; if ( vespene === undefined ) { vespene = incomeVespene ( this . workersVespene , this . gasCount , this . baseCount ) / + this . settings . incomeFactor ; vespeneIncomeCache [ [ this . workersVespene , this . gasCount ] ] = vespene ; } this . minerals += minerals ; this . vespene += vespene ; } canAfford ( unit ) { console . assert ( unit . name , JSON . stringify ( unit , null , 4 ) ) ; console . assert ( unit . type , JSON . stringify ( unit , null , 4 ) ) ; if ( unit . type === "upgrade" ) { return this . _canAfford ( this . getCost ( unit . name , true ) ) ; } else { return this . _canAfford ( this . getCost ( unit . name , false ) ) ; } } _canAfford ( cost ) { console . assert ( cost . minerals , JSON . stringify ( cost , null , 4 ) ) ; return ( cost . minerals <= this . minerals && cost . vespene <= this . vespene && ( cost . supply < 0 || cost . supply <= this . supplyLeft ) ) ; } canRequirementBeDuplicated ( requirementName , itemName ) { const itemInfo = TRAINED_BY [ itemName ] ; const isMorphedFromAnotherUnit = itemInfo && itemInfo . requiresUnits && itemInfo . requiresUnits . includes ( requirementName ) ; const isArchonMaterial = ! itemInfo && ( ( itemName === "morph_archon_from_ht_ht" && requirementName === "HighTemplar" ) || ( itemName === "morph_archon_from_dt_dt" && requirementName === "DarkTemplar" ) || ( itemName === "morph_archon_from_ht_dt" && ( requirementName === "HighTemplar" || requirementName === "DarkTemplar" ) ) ) ; const isSupplyProvider = requirementName === supplyUnitNameByRace [ this . race ] . name ; return isMorphedFromAnotherUnit || isArchonMaterial || isSupplyProvider ; } static simulatedBuildOrder ( prevGamelogic , buildOrder ) { const gamelogic = new GameLogic ( prevGamelogic . race , buildOrder , prevGamelogic . customSettings , prevGamelogic . customOptimizeSettings ) ; gamelogic . setStart ( ) ; gamelogic . runUntilEnd ( ) ; return gamelogic ; } static addItemToBO ( prevGamelogic , item , insertIndex ) { const bo = prevGamelogic . bo ; const initialBOLength = bo . length ; if ( item . type === "upgrade" && prevGamelogic . upgrades . has ( item . name ) ) { return [ prevGamelogic , insertIndex ] ; } bo . splice ( insertIndex , 0 , item ) ; let gamelogic = GameLogic . simulatedBuildOrder ( prevGamelogic , bo ) ; let fillingLoop = 0 ; if ( insertIndex === bo . length - 1 && ! prevGamelogic . errorMessage ) { do { if ( fillingLoop > 0 ) { gamelogic = GameLogic . simulatedBuildOrder ( prevGamelogic , bo ) ; } if ( gamelogic . errorMessage && gamelogic . requirements ) { fillingLoop ++ ; const duplicatesToRemove = [ ] ; for ( const req of gamelogic . requirements ) { let duplicateItem ; if ( ! gamelogic . canRequirementBeDuplicated ( req . name , item . name ) ) { duplicateItem = find ( bo , req ) ; } if ( ! duplicateItem || bo . indexOf ( duplicateItem ) >= insertIndex ) { bo . splice ( insertIndex , 0 , req ) ; if ( duplicateItem ) { duplicatesToRemove . push ( duplicateItem ) ; } } } for ( const duplicate of duplicatesToRemove ) { remove ( bo , ( item ) => item === duplicate ) ; } } } while ( gamelogic . errorMessage && gamelogic . requirements && fillingLoop < 25 ) ; } const insertedItems = bo . length - initialBOLength ; return [ gamelogic , insertedItems ] ; } } export { Event , GameLogic } ;