//============================================================================== /* aiEventHandlers.xs Contains the even handler methods for the AI */ //============================================================================== //============================================================================== // getClassRating() // // Get a class rating, 0.0 to 1.0, for this type of opportunity. // Scores zero when an opportunity of this type was just launched. // Scores 1.0 when it has been 'gXXXMissionInterval' time since the last one. //============================================================================== float getClassRating(int oppType = -1, int target = -1) { float retVal = 1.0; float timeElapsed = 0.0; int targetType = -1; int attackInterval = gAttackMissionInterval; float rushBoom = btRushBoom; // Don't ever increase the attack interval if (rushBoom > 0.0) rushBoom = 0.0; // For rushing, decrease us faster else { rushBoom = rushBoom * 2.0; if (rushBoom < -1.0) rushBoom = -1.0; } attackInterval = attackInterval + btRushBoom * gAttackMissionInterval; switch(oppType) { case cOpportunityTypeDestroy: { if (gLastAttackMissionTime > 0) { timeElapsed = xsGetTime() - gLastAttackMissionTime; retVal = 1.0 * (timeElapsed / gAttackMissionInterval); } else retVal = 1.0; break; } case cOpportunityTypeDefend: { timeElapsed = xsGetTime() - gLastDefendMissionTime; retVal = 1.0 * (timeElapsed / gDefendMissionInterval); // [5/14/2009 CJS] On quests, we should only attack if (aiIsQuestMap() == true) retVal = 0.0; break; } } if (retVal > 1.0) retVal = 1.0; if (retVal < 0.0) retVal = 0.0; return(retVal); } //============================================================================== // scoreOpportunity() // // Called for each opportunity that needs to be scored. //============================================================================== void scoreOpportunity(int oppID = -1) { // Sets all the scoring components for the opportunity, and a final score. The scoring // components and their meanings are: // // int PERMISSION What level of permission is needed to do this? // cOpportunitySourceAutoGenerated is the lowest...go ahead and do it. // cOpportunitySourceAllyRequest...the AI may not do it on its own, i.e. it may be against the rules for this difficulty. // cOpportunitySourceTrigger...even ally requests are denied, as when prevented by control variables, but a trigger (gaia request) may do it. // cOpportunitySourceTrigger+1...not allowed at all. // // float AFFORDABLE Do I have what it takes to do this? This includes appropriate army sizes, resources to pay for things (like trading posts) // and required units like explorers. 0.80 indicates a neutral, good-to-go position. 1.0 means overstock, i.e. an army of 20 would be good, // and I have 35 units available. 0.5 means extreme shortfall, like the minimum you could possibly imagine. 0.0 means you simply can't do it, // like no units at all. Budget issues like amount of wood should never score below 0.5, scores below 0.5 mean deep, profound problems. // // int SOURCE Who asked for this mission? Uses the cOpportunitySource... constants above. // // float CLASS How much do we want to do this type of mission? Based on personality, how long it's been since the last mission of this type, etc. // 0.8 is a neutral, "this is a good mission" rating. 1.0 is extremely good, I really, really want to do this next. 0.5 is a poor score. 0.0 means // I just flat can't do it. This class score will creep up over time for most classes, to make sure they get done once in a while. // // float INSTANCE How good is this particular target? Includes asset value (is it important to attack or defend this?) and distance. Defense values // are incorporated in the AFFORDABLE calculation above. 0.0 is no value, this target can't be attacked. 0.8 is a good solid target. 1.0 is a dream target. // // float TOTAL Incorporates AFFORDABLE, CLASS and INSTANCE by multiplying them together, so a zero in any one sets total to zero. Source is added as an int // IF AND ONLY IF SOURCE >= PERMISSION. If SOURCE < PERMISSION, the total is set to -1. Otherwise, all ally source opportunities will outrank all self generated // opportunities, and all trigger-generated opportunities will outrank both of those. Since AFFORDABLE, CLASS and INSTANCE all aim for 0.8 as a good, solid // par value, a total score of .5 is rougly "pretty good". A score of 1.0 is nearly impossible and should be quite rare...a high-value target, weakly defended, // while I have a huge army and the target is close to me and we haven't done one of those for a long, long time. // // Total of 0.0 is an opportunity that should not be serviced. >0 up to 1 indicates a self-generated opportunity, with 0.5 being decent, 1.0 a dream, and 0.2 kind // of marginal. Ally commands are in the range 1.0 to 2.0 (unless illegal), and triggers score 2.0 to 3.0. // Interim values for the scoring components: int permission = 0; float instance = 0.0; float classRating = 0.0; float total = 0.0; float affordable = 0.0; float score = 0.0; // Info about this opportunity int source = aiGetOpportunitySourceType(oppID); if (source < 0) source = cOpportunitySourceAutoGenerated; if (source > cOpportunitySourceTrigger) source = cOpportunitySourceTrigger; int target = aiGetOpportunityTargetID(oppID); int targetType = aiGetOpportunityTargetType(oppID); int oppType = aiGetOpportunityType(oppID); int targetPlayer = aiGetOpportunityTargetPlayerID(oppID); vector location = aiGetOpportunityLocation(oppID); float radius = aiGetOpportunityRadius(oppID); if (radius < 10.0) radius = 40.0; int baseOwner = -1; float baseEnemyPower = 0.0; // Used to measure troop and building strength. Units roughly equal to unit count of army. float baseAllyPower = 0.0; // Strength of allied buildings and units, roughly equal to unit count. float netEnemyPower = 0.0; // Basically enemy minus ally, but the ally effect can, at most, cut 80% of enemy strength float baseAssets = 0.0; // Rough estimate of base value, in aiCost. float affordRatio = 0.0; bool errorFound = false; // Set true if we can't do a good score. Ends up setting score to -1. // Variables for available number of units and plan to kill if any float armySizeAuto = 0.0; // For source cOpportunitySourceAutoGenerated float armySizeAlly = 0.0; // For ally-generated commands, how many units could we scrounge up? int missionToKillAlly = -1; // Mission to cancel in order to provide the armySizeAlly number of units. float armySizeTrigger = 0.0; // For trigger-generated commands, how many units could we scrounge up? int missionToKillTrigger = -1; // Mission to cancel in order to provide the armySizeTrigger number of units. float armySize = 0.0; // The actual army size we'll use for calcs, depending on how big the target is. float missionToKill = -1; // The actual mission to kill based on the army size we've selected. float oppDistance = 0.0; // Distance to target location or base. bool sameAreaGroup = true; // Set false if opp is on another areagroup. //-- get the number of units in our reserve. armySizeAuto = aiPlanGetNumberUnits(gLandReservePlan, cUnitTypeLogicalTypeLandMilitary); armySizeAlly = armySizeAuto; armySizeTrigger = armySizeAlly; /*aiEcho(" "); switch (oppType) { case cOpportunityTypeDestroy: { aiEcho("Scoring opportunity "+oppID+", targetID "+target+", location "+location+", type cOpportunityTypeDestroy"); break; } case cOpportunityTypeDefend: { aiEcho("Scoring opportunity "+oppID+", targetID "+target+", location "+location+", type cOpportunityTypeDefend"); break; } default: { aiEcho("Scoring opportunity "+oppID+", targetID "+target+", location "+location+", type "+oppType); break; } }*/ // Get target info switch(targetType) { case cOpportunityTargetTypeBase: { location = kbBaseGetLocation(kbBaseGetOwner(target),target); radius = 50.0; baseOwner = kbBaseGetOwner(target); baseEnemyPower = getBaseEnemyStrength(target); // Calculate "defenses" as enemy units present baseAllyPower = getPointAllyStrength(kbBaseGetLocation(kbBaseGetOwner(target),target)); if ( (baseEnemyPower*0.8) > baseAllyPower) netEnemyPower = baseEnemyPower - baseAllyPower; // Ally power is less than 80% of enemy else netEnemyPower = baseEnemyPower * 0.2; // Ally power is more then 80%, but leave a token enemy rating anyway. baseAssets = getBaseValue(target); // Rough value of target break; } case cOpportunityTargetTypePointRadius: { baseEnemyPower = getPointEnemyStrength(location); baseAllyPower = getPointAllyStrength(location); if ( (baseEnemyPower*0.8) > baseAllyPower) netEnemyPower = baseEnemyPower - baseAllyPower; // Ally power is less than 80% of enemy else netEnemyPower = baseEnemyPower * 0.2; // Ally power is more then 80%, but leave a token enemy rating anyway. baseAssets = getPointValue(location); // Rough value of target break; } case cOpportunityTargetTypeUnitList: { baseEnemyPower = getPointEnemyStrength(location); baseAllyPower = getPointAllyStrength(location); if ( (baseEnemyPower*0.8) > baseAllyPower) netEnemyPower = baseEnemyPower - baseAllyPower; // Ally power is less than 80% of enemy else netEnemyPower = baseEnemyPower * 0.2; // Ally power is more then 80%, but leave a token enemy rating anyway. baseAssets = getPointValue(location); // Rough value of target break; } } if (netEnemyPower < 1.0) netEnemyPower = 1.0; // Avoid div 0 oppDistance = distance(location, kbBaseGetLocation(cMyID, kbBaseGetMainID(cMyID))); if (oppDistance <= 0.0) oppDistance = 1.0; if ( kbAreaGroupGetIDByPosition(location) != kbAreaGroupGetIDByPosition(kbBaseGetLocation(cMyID, kbBaseGetMainID(cMyID))) ) sameAreaGroup = false; // Figure which armySize to use. This currently is a placeholder, we may not need to mess with it. armySize = armySizeAuto; // Default //aiEcho(" EnemyPower "+baseEnemyPower+", AllyPower "+baseAllyPower+", NetEnemyPower "+netEnemyPower); //aiEcho(" BaseAssets "+baseAssets+", myArmySize "+armySize); switch(oppType) { case cOpportunityTypeDestroy: { // Check permissions required. if(cvOkToAttack == false) permission = cOpportunitySourceTrigger; // Only triggers can make us attack. // [5/13/2009 CJS] Put back? //if (gDelayAttacks == true) //permission = cOpportunitySourceTrigger; // Only triggers can override this difficulty setting. // Check affordability if (netEnemyPower < 0.0) { errorFound = true; affordable = 0.0; } else { // Set affordability. Roughly armySize / baseEnemyPower, but broken into ranges. // 0.0 is no-can-do, i.e. no troops. 0.8 is "good", i.e. armySize is double baseEnemyPower. // Above a 2.0 ratio, to 5.0, scale this into the 0.8 to 1.0 range. // Above 5.0, score it 1.0 affordRatio = armySize / netEnemyPower; if (affordRatio < 2.0) affordable = affordRatio / 2.5; // 0 -> 0.0, 2.0 -> 0.8 else affordable = 0.8 + ((affordRatio - 2.0) / 15.0); // 2.0 -> 0.8 and 5.0 -> 1.0 if (affordable > 1.0) affordable = 1.0; } // Affordability is done // Check target value, calculate INSTANCE score. if (baseAssets < 0.0) { errorFound = true; } // Clip base value to range of 100 to 10K for scoring if (baseAssets < 100.0) baseAssets = 100.0; if (baseAssets > 10000.0) baseAssets = 10000.0; // Start with an "instance" score of 0 to .8 for bases under 2K value. instance = (0.8 * baseAssets) / 2000.0; // Over 2000, adjust so 2K = 0.8, 30K = 1.0 if (baseAssets > 2000.0) instance = 0.8 + ( (0.2 * (baseAssets - 2000.0)) / 8000.0); // Instance is now 0..1, adjust for distance. If < 100m, leave as is. Over 100m to 400m, penalize 5% per 100m. float penalty = 0.0; if (oppDistance > 100.0) penalty = (0.05 * (oppDistance - 100.0)) / 100.0; if (penalty > 0.6) penalty = 0.6; instance = instance * (1.0 - penalty); // Apply distance penalty, INSTANCE score is done. if (sameAreaGroup = false) instance = instance / 2.0; if (targetType == cOpportunityTargetTypeBase) if (kbHasPlayerLost(baseOwner) == true) instance = -1.0; // Illegal if it's over water, i.e. a lone dock if (kbAreaGetType(kbAreaGetIDByPosition(location)) == cAreaTypeWater) instance = -1.0; // Check for weak target blocks, which means the content designer is telling us that this target needs its instance score bumped up /*int weakBlockCount = 0; int strongBlockCount = 0; if ( targetType == cOpportunityTargetTypeBase) { weakBlockCount = getUnitCountByLocation(cUnitTypeAITargetBlockWeak, cMyID, cUnitStateAlive, kbBaseGetLocation(baseOwner, target), 40.0); strongBlockCount = getUnitCountByLocation(cUnitTypeAITargetBlockStrong, cMyID, cUnitStateAlive, kbBaseGetLocation(baseOwner, target), 40.0); } if ( (targetType == cOpportunityTargetTypeBase) && (weakBlockCount > 0) && (instance >= 0.0) ) { // We have a valid instance score, and there is at least one weak block in the area. For each weak block, move the instance score halfway to 1.0. while (weakBlockCount > 0) { instance = instance + ((1.0-instance) / 2.0); // halfway up to 1.0 weakBlockCount--; } }*/ classRating = getClassRating(cOpportunityTypeDestroy); // 0 to 1.0 depending on how long it's been. /*if ( (targetType == cOpportunityTargetTypeBase) && (strongBlockCount > 0) && (classRating >= 0.0) ) { // We have a valid instance score, and there is at least one strong block in the area. For each weak block, move the classRating score halfway to 1.0. while (strongBlockCount > 0) { classRating = classRating + ((1.0-classRating) / 2.0); // halfway up to 1.0 strongBlockCount--; } }*/ if (aiTreatyActive() == true) classRating = 0.0; // Do not attack anything if under treaty break; } case cOpportunityTypeRaid: { break; } case cOpportunityTypeDefend: { // Check affordability if (netEnemyPower < 0.0) { errorFound = true; affordable = 0.0; } else { // Set affordability. Roughly armySize / netEnemyPower, but broken into ranges. // Very different than attack calculations. Score high affordability if the ally is really // in trouble, especially if my army is large. Basically...does he need help? Can I help? if (baseAllyPower < 1.0) baseAllyPower = 1.0; float enemyRatio = baseEnemyPower / baseAllyPower; float enemySurplus = baseEnemyPower - baseAllyPower; if (enemyRatio < 0.5) // Enemy very weak, not a good opp. { affordRatio = enemyRatio; // Low score, 0 to .5 if (enemyRatio < 0.2) affordRatio = 0.0; } else affordRatio = 0.5 + ( (enemyRatio - 0.5) / 5.0); // ratio 0.5 scores 0.5, ratio 3.0 scores 1.0 if ( (affordRatio * 10.0) > enemySurplus ) affordRatio = enemySurplus / 10.0; // Cap the afford ratio at 1/10 the enemy surplus, i.e. don't respond if he's just outnumbered 6:5 or something trivial. if (enemySurplus < 0) affordRatio = 0.0; if (affordRatio > 1.0) affordRatio = 1.0; // AffordRatio now represents how badly I'm needed...now, can I make a difference if (armySize < enemySurplus) // I'm gonna get my butt handed to me affordRatio = affordRatio * (armySize / enemySurplus); // If I'm outnumbered 3:1, divide by 3. // otherwise, leave it alone. affordable = affordRatio; } // Affordability is done // Check target value, calculate INSTANCE score. if (baseAssets < 0.0) { errorFound = true; } // Clip base value to range of 100 to 30K for scoring if (baseAssets < 100.0) baseAssets = 100.0; if (baseAssets > 30000.0) baseAssets = 30000.0; // Start with an "instance" score of 0 to .8 for bases under 2K value. instance = (0.8 * baseAssets) / 1000.0; // Over 1000, adjust so 1K = 0.8, 30K = 1.0 if (baseAssets > 1000.0) instance = 0.8 + ( (0.2 * (baseAssets - 1000.0)) / 29000.0); // Instance is now 0..1, adjust for distance. If < 200m, leave as is. Over 200m to 400m, penalize 10% per 100m. penalty = 0.0; if (oppDistance > 200.0) penalty = (0.1 * (oppDistance - 200.0)) / 100.0; if (penalty > 0.6) penalty = 0.6; instance = instance * (1.0 - penalty); // Apply distance penalty, INSTANCE score is done. if (sameAreaGroup == false) instance = 0.0; if (targetType == cOpportunityTargetTypeBase) if (kbHasPlayerLost(baseOwner) == true) instance = -1.0; classRating = getClassRating(cOpportunityTypeDefend); // 0 to 1.0 depending on how long it's been. break; } default: { aiEcho("ERROR ERROR ERROR ERROR"); aiEcho("scoreOpportunity() failed on opportunity "+oppID); aiEcho("Opportunity Type is "+oppType+" (invalid)"); break; } } // [5/13/2009 CJS] HACK affordable = 1.0; score = classRating * instance * affordable; // [5/14/2009 CJS] Make the AI more likely to "do stuff" on quests if (aiIsQuestMap() == true) score = score + 0.3; //aiEcho(" Class "+classRating+", Instance "+instance+", affordable "+affordable); //aiEcho(" Final Score: "+score); if (score > 1.0) score = 1.0; if (score < 0.0) score = 0.0; score = score + source; // Add 1 if from ally, 2 if from trigger. if (permission > source) score = -1.0; if (errorFound == true) score = -1.0; if (cvOkToSelectMissions == false) score = -1.0; // If we're in WTS mode, go when we have a wave if (oppType == cOpportunityTypeDestroy && aiGetUseWaves() == true) { if (gHasWave) { classRating = 1.0; score = 1.0; } } aiSetOpportunityScore(oppID, permission, affordable, classRating, instance, score); } //============================================================================== // missionStartHandler() // // Track times for mission starts, so we can tell how long its been since // we had a mission of a given type. //============================================================================== void missionStartHandler(int missionID = -1) { if (missionID < 0) return; int oppID = aiPlanGetVariableInt(missionID, cMissionPlanOpportunityID, 0); int oppType = aiGetOpportunityType(oppID); //aiPlanSetVariableInt(missionID, cMissionPlanStartTime, 0, xsGetTime()); // Set the start time in ms. aiPlanSetVariableInt(missionID, cAttackPlanPlayerID, 0, xsGetTime()); // Set the start time in ms. switch(oppType) { case cOpportunityTypeDestroy: { gNumAttacksLaunched = gNumAttacksLaunched + 1; btAttacksThisAge = btAttacksThisAge + 1; if (gNumAttacksLaunched == btAttackGroups) { gLastAttackMissionTime = xsGetTime(); gNumAttacksLaunched = 0; } aiEcho("-------- ATTACK MISSION ACTIVATION: Mission "+missionID+", Opp "+oppID); // Reset military size and wave flag gMaxUnitLines = gMaxUnitLines + 1; // This will be capped to the current age in updateMilitarySize(). updateMilitarySize(); gHasWave = false; break; } case cOpportunityTypeDefend: { gLastDefendMissionTime = xsGetTime(); aiEcho("-------- DEFEND MISSION ACTIVATION: Mission "+missionID+", Opp "+oppID); break; } default: { aiEcho("-------- UNKNOWN MISSION ACTIVATION: Mission "+missionID+", Opp "+oppID); break; } } } //============================================================================== // missionEndHandler() // // Called when the AI ends a mission //============================================================================== void missionEndHandler(int missionID = -1) { aiEcho("-------- MISSION TERMINATION: Mission "+missionID+", Opp "+aiGetOpportunityType(aiPlanGetVariableInt(missionID, cMissionPlanOpportunityID, 0))); } //============================================================================== //============================================================================== void cleanupEventHandlers() { // Nothing to do here for now } //============================================================================== // AgeUpHandler() // // Called when a player ages up //============================================================================== void ageUpHandler(int playerID = -1) { // Nothing to do here for now }