diff --git a/README.md b/README.md index 141c155..07760e4 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,9 @@ You can download a release of this AI that is considered to be "the most stable" All releases are marked with git tags in this repository. This AI requires Warzone 2100 v3.2 or higher to work. Unless a time machine has been invented, this means that you'd need to get a nightly development snapshot of the game's master version at http://buildbot.wz2100.net/files/ Please use the actually-latest snapshot, because a lot of work is currently being done on the scripting system. + +Harold +======= +Harold is my take on expanding NullBot's interaction with human players, using Automated Personalites. By that, this means you, the player, will be given chat messages from this modfied NullBot, whether you are friend or foe. Although, haoNoQ made this impressive AI exist, not many players felt pressured to take on this AI and it's adaptive strategies. So, by giving this AI a more talkative aspect, the Player can engage in mind games with this AI. This, in theory, might result in more player victories over this AI, when the player successfully pressures the AI into making rash decisions. + +The ultimate goal of Harold is to make Skirmishes against the AI, fun again. Any suggestions are allowed, but only if they are within scope of this experiment. diff --git a/TO_DO b/TO_DO new file mode 100644 index 0000000..041f437 --- /dev/null +++ b/TO_DO @@ -0,0 +1,21 @@ +To-do List for Harold - + +High Priority: +* Teach Harold to Chat to Human Opponent. Include following responses for following skirmish situations: +- Greeting upon startup +- Be polite when given polite response from human player (0). +- Otherwise, act like an arse when, when human player responds like such. +- On EASY, declare to player when Harold is "going on the offensive" to serve as warning. +- On EASY, declare to player when Harold has built any structure, besides Factory, Research Center, and/or Command Center. +- On EASY, declare to player when Harold has produced the first of any droid and/or cyborg, besides Construction Droid. +- On EASY, declare to player when Harold catches player is using cowardly tactics (examples of cowardly tactics do not include +use of debug cheats, rather not applying self to research new tech, thus relying on the same droids, and constantly fleeing +from enemy presence. As new info on the subject becomes public, updates will follow.) +- On EASY (and beyond), declare to player when Harold catches player using Cheats, and warn player when Harold will use them, as well. + +Low Prioity: +- Declare to player unconditional surrender, when Harold is outmatched by player. +- Praise player when player's attacking force defeats opponent Harold's defending force. +- Declare vengeful response when Harold successfully eliminates dominating player force. +- Declare dominating response when Harold continues to eliminate player reinforcements with dominating force. +- Taunt player. \ No newline at end of file diff --git a/har_includes-custom/_events.js b/har_includes-custom/_events.js new file mode 100644 index 0000000..156a655 --- /dev/null +++ b/har_includes-custom/_events.js @@ -0,0 +1,105 @@ + +/* + * This file includes event definitions only. + * + */ + +function eventStartLevel() { + queue("setTimers", me * 100); + if (alliancesType === ALLIANCES_TEAMS) { + // initialize subpersonality pseudo-randomly here + // to make sure teammates have the same personality + var j = 1, s = 0; + for (var i = 0; i < maxPlayers; ++i) { + if (allianceExistsBetween(me, i)) + s += j; + j *= 2; + } + // the random "s" number obtained here is the same for all players in any team + var s = s + (new Date()).getMinutes(); + j = 0; + for (var i in subpersonalities) // count the amount of subpersonalities + ++j; + s = s % j; + j = 0; + for (var i in subpersonalities) { + if (j === s) + personality = subpersonalities[i]; + ++j; + } + } else { + // if teams are not sharing research, or there are no teams at all, + // initialize the subpersonality randomly and don't care + personality = randomItem(subpersonalities); + } + // the following code is necessary to avoid some strange game bug when droids that + // are initially buried into the ground fail to move out of the way when a building + // is being placed right above them + enumTrucks().forEach(function(droid) { + orderDroidLoc(droid, DORDER_MOVE, droid.x + random(3) - 1, droid.y + random(3) - 1); + }); +} + +function eventDroidBuilt(droid, structure) { + groupDroid(droid); +} + +function eventStructureBuilt(structure) { + queue("checkConstruction"); +} + +function eventAttacked(victim, attacker) { + if (attacker === null) + return; // no idea why it happens sometimes + if (isAlly(attacker.player)) + return; // don't respond to accidental friendly fire + if (victim.type === DROID) { + if (!isVTOL(victim) && defined(victim.group)) { + fallBack(victim, attacker); + setTarget(attacker, victim.group); + touchGroup(victim.group); + } else if (isVTOL(victim) && victim.player == me && !throttled(5000, victim.id)) { + orderDroidObj(victim, DORDER_ATTACK, attacker); + pushVtols(attacker); + } + } else if (victim.type === STRUCTURE) { + if (throttled(5000) && victim.player != me) + return; + if (inPanic()) + for (var i = 0; i < MAX_GROUPS; ++i) + if (groupSize(i) > 0) + setTarget(attacker, i); + setTarget(attacker, miscGroup); + setTarget(attacker); + } +} + +function eventStructureReady(structure) { + fireLassat(structure); +} + +function eventChat(from, to, message) { + // we are not case-sensitive + message = message.toLowerCase(); + handleChatMessage(from, to, message) +} + +function eventObjectTransfer(object, from) { + if (object.player !== me) + return; // object was transferred from me, not to me + if (object.type === DROID) + groupDroid(object); +} + +function eventBeacon(x, y, from, to) { + noticeBeacon(x, y, from); +} + +function eventBeaconRemoved(from, to) { + unnoticeBeacon(from); +} + +function eventDestroyed(object) { + if (isEnemy(object.player)) + pushVtols(object); +} diff --git a/har_includes-custom/_head.js b/har_includes-custom/_head.js new file mode 100644 index 0000000..75df8ef --- /dev/null +++ b/har_includes-custom/_head.js @@ -0,0 +1,107 @@ + +/* + * This file defines generic things that should be defined + * prior to defining a ruleset. + * + * NOTE: This file is not included from main.js . + * + */ + +// weapon path role +const ROLE = { + AT: 0, + AP: 1, + AS: 2, + AA: 3, + LENGTH: 4, // number of items in this enum +} + +// something dual to the previous enum +const OBJTYPE = { + TANK: 0, + BORG: 1, + DEFS: 2, + VTOL: 3, + LENGTH: 4, // number of items in this enum; should be equal to ROLE.LENGTH +} + +// this controls body and weapon compatibility. +// A little explanation: +// w \ b | L | M | H bodies can't be "ultra-", +// UL | + | - | - ultra-light weapons are for light bodies only, +// L | + | + | - light weapons are for light or medium bodies, +// M | + | + | + medium weapons are for all bodies, +// H | - | + | + heavy weapons are for medium or heavy bodies, +// UH | - | - | + ultra-heavy weapons are for heavy bodies only. +const WEIGHT = { + ULTRALIGHT: 0, + LIGHT: 1, + MEDIUM: 2, + HEAVY: 3, + ULTRAHEAVY: 4, +} + +// what to use this defensive structure for +const DEFROLE = { + STANDALONE: 0, + GATEWAY: 1, + ARTY: 2, + ANTIAIR: 3, + FORTRESS: 4, +} + +// return values of build order calls +const BUILDRET = { + SUCCESS: 0, + UNAVAILABLE: 1, + FAILURE: 2, +} + +// should we execute a build call in an unsafe location? +const IMPORTANCE = { + PEACETIME: 0, + MANDATORY: 1, +} + +// aspects of every research path +const RESASPECTS = { + WEAPONS: 0, + DEFENSES: 1, + VTOLS: 2, + EXTRAS: 3, + LENGTH: 4, // number of items in this enum +} + +const PROPULSIONUSAGE = { + GROUND : 1, + HOVER: 2, + VTOL: 4 +} + +// what to use this body for? (bit field) +const BODYUSAGE = { + GROUND: 1, // for tanks + AIR: 2, // for VTOLs + COMBAT: 3, // GROUND | AIR + TRUCK: 4, // trucks, sensors and repair droids + UNIVERSAL: 7, // COMBAT | TRUCK +} + +// what sort of weapons this body is resistant to? +const BODYCLASS = { + KINETIC: 1, + THERMAL: 2, +} + +// should this module be prioritized when not having enough power? +const MODULECOST = { + CHEAP: 0, + EXPENSIVE: 1, +} + +// how to micromanage tanks with this sort of weapon +const MICRO = { + RANGED: 0, // normal weapons, such as cannons + MELEE: 1, // weapons that need to get very close to the enemy, such as flamers + DUMB: 2, // weapons that can't fire while moving +} diff --git a/har_includes-custom/_main.js b/har_includes-custom/_main.js new file mode 100644 index 0000000..3e15d9a --- /dev/null +++ b/har_includes-custom/_main.js @@ -0,0 +1,26 @@ + +/* + * This file connects all remaining pieces of AI code together. + * It shouldn't contain any code itself. + * + * NOTE: order matters! + * + */ + +var personality; + +include(HAR_INCLUDES + "math.js"); +include(HAR_INCLUDES + "intensity.js"); +include(HAR_INCLUDES + "misc.js"); +include(HAR_INCLUDES + "stats.js"); + +include(HAR_INCLUDES + "adapt.js"); +include(HAR_INCLUDES + "build.js"); +include(HAR_INCLUDES + "research.js"); +include(HAR_INCLUDES + "produce.js"); +include(HAR_INCLUDES + "tactics.js"); +include(HAR_INCLUDES + "lassat.js"); +include(HAR_INCLUDES + "chat.js"); + +include(HAR_INCLUDES + "timers.js"); +include(HAR_INCLUDES + "_events.js"); diff --git a/har_includes-custom/adapt.js b/har_includes-custom/adapt.js new file mode 100644 index 0000000..3c05501 --- /dev/null +++ b/har_includes-custom/adapt.js @@ -0,0 +1,484 @@ + +/* + * This file is responsible for the adaptation system. It gathers statistics about + * player choices and regulates the AI's choices accordingly. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +function adaptVote(our, their, verbose) { + if (!defined(verbose)) + verbose = false; + var l = our.length; + var ourTotal = our.reduce(function(prev, curr) { return prev + curr; }); + var theirTotal = their.reduce(function(prev, curr) { return prev + curr; }); + if (theirTotal === 0) + return verbose ? randomUnitArray(l) : random(l); + var rating = []; + for (var i = 0; i < l; ++i) + rating[i] = their[i] / theirTotal; + if (ourTotal > 0) + for (var i = 0; i < l; ++i) + rating[i] -= our[i] / ourTotal; + if (verbose) + return rating.map(function(val) { return (val + 1) / 2; }); + var maxRating = -Infinity; + var maxIdx = 0; + for (var i = 0; i < l; ++i) + if (rating[i] > maxRating) { + maxRating = rating[i]; + maxIdx = i; + } + return maxIdx; +} + +// +// here be functions for gathering player statistics +// + +function SimpleStat() { + // propulsion classes used by the player + this.obj = zeroArray(ROLE.LENGTH); + // weapon-propulsion classes used by the player + this.role = zeroArray(ROLE.LENGTH); + // armor used by the player + this.kbody = 0; + this.tbody = 0; + // weapon-armor classes used by the player + this.kweapon = 0; + this.tweapon = 0; +} + +function addStat(to, what, weight) { + if (!defined(weight)) + weight = 1; + for (var prop in to) { + if (to[prop].constructor === Array) + for (var i = 0; i < to[prop].length; ++i) + to[prop][i] += what[prop][i] * weight; + else + to[prop] += what[prop] * weight; + } +} + +const SCOPES = { + land: 0, + sea: 0, + air: 0, +} + +function ScopeStat() { + // simple stats by scopes + this.land = new SimpleStat(); + this.sea = new SimpleStat(); + this.air = new SimpleStat(); + // a function to convert this sort of stat to a SimpleStat + this.collapse = function() { + var ret = new SimpleStat(); + for (var i in SCOPES) + addStat(ret, this[i]); + return ret; + } +} + +function EnemyStat() { + // separate stats for defense and offense + this.defense = new ScopeStat(); + this.offense = new SimpleStat(); + // a function to convert this sort of stat to a SimpleStat + this.collapse = function() { + var ret = new SimpleStat(); + addStat(ret, this.defense.collapse()); + addStat(ret, this.offense); + return ret; + } +} + +function MyStat() { + // separate stats for defense and offense + this.defense = new SimpleStat(); + this.offense = new ScopeStat(); + // a function to convert this sort of stat to a SimpleStat + this.collapse = function() { + var ret = new SimpleStat(); + addStat(ret, this.defense); + addStat(ret, this.offense.collapse()); + return ret; + } +} + +var enemyStats = [], enemyStatsTemp = []; +for (var i = 0; i < maxPlayers; ++i) { + enemyStats[i] = new EnemyStat; + enemyStatsTemp[i] = new EnemyStat; +} + +var myStats = new MyStat; +var myStatsTemp = new MyStat; + +var stack = []; +var MAX_PER_CYCLE = 20; + +function canReachBy(scope, location) { + switch (scope) { + case "land": + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND).last(), location); + case "sea": + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER).last(), location); + case "air": + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.AIR).last(), location); + } +} + +// works with stored droid objects too! +function threatensBase(droid) { + if (isAlly(droid.player)) + return false; + return canReachFromBase(droid.propulsion, droid.x, droid.y); +} + +// count target structures reachable by land +function countLandTargets(player) { + function uncached() { + var currProp = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND).last(); + if (!defined(currProp)) + return 0; + var list = enumStructList(targets, player); + return list.filter(function(struct) { + return canReachFromBase(currProp, struct); + }).length; + } + return cached(uncached, 5000, player); +} + +// count target structures reachable by sea but not by land +function countSeaTargets(player) { + function uncached() { + var currProp = getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER)[0]; + if (!defined(currProp)) + return 0; + var prevProp = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND)[0]; + return enumStructList(targets, player).filter(function(struct) { + return (!defined(prevProp) || !canReachFromBase(prevProp, struct)) && canReachFromBase(currProp, struct); + }).length; + } + return cached(uncached, 5000, player); +} + +// count target structures reachable by air but not by land or by sea +function countAirTargets(player) { + function uncached() { + var currProp = getPropulsionStatsComponents(PROPULSIONUSAGE.VTOL)[0]; + if (!defined(currProp)) + return 0; + var prevProp = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND|PROPULSIONUSAGE.HOVER)[0]; + return enumStructList(targets, player).filter(function(struct) { + return (!defined(prevProp) || !canReachFromBase(prevProp, struct)) && canReachFromBase(currProp, struct); + }).length; + } + return cached(uncached, 5000, player); +} + +// TODO: this function needs to be updated once more object properties +// become available for the API to use. +function classifyObject(obj) { + var ret = new SimpleStat(); + if (obj.type === STRUCTURE && obj.stattype !== DEFENSE) + return ret; + if (obj.type === DROID && obj.droidType !== DROID_WEAPON && obj.droidType !== DROID_CYBORG) + return ret; + if (obj.type === FEATURE) + return ret; + for (var i = 0; i < obj.weapons.length; ++i) { + var roles = guessWeaponRole(obj.weapons[i].name); { + if (!defined(roles)) { + if (obj.canHitAir && obj.canHitGround) + ret.role.addArray([1/4, 1/4, 1/4, 1/4]); + else if (obj.canHitAir && !obj.canHitGround) + ret.role.addArray([0, 0, 0, 1]); + else + ret.role.addArray([1/3, 1/3, 1/3, 0]); + } else + ret.role.addArray(roles); + } + } + for (var i = 0; i < obj.weapons.length; ++i) { + if (getWeaponInfo(obj.weapons[i].name).impactClass === "KINETIC") + ret.kweapon += 1; + else + ret.tweapon += 1; + } + if (obj.type === STRUCTURE || (obj.type === DROID && safeIsVtol(obj))) + ret.tbody += 1; + else if (obj.type === DROID && obj.droidType === DROID_CYBORG) + ret.kbody += 1; + else { + switch (guessBodyArmor(obj.body)) { + case BODYCLASS.KINETIC: + ret.kbody += 1; + break; + case BODYCLASS.THERMAL: + ret.tbody += 1; + break; + default: + ret.tbody += 1 / 2; + ret.kbody += 1 / 2; + } + } + if (obj.type === STRUCTURE) + ret.obj[OBJTYPE.DEFS] += 1; + if (obj.type === DROID) { + if (safeIsVtol(obj)) + ret.obj[OBJTYPE.VTOL] += 1; + else if (obj.droidType === DROID_CYBORG) + ret.obj[OBJTYPE.BORG] += 1; + else + ret.obj[OBJTYPE.TANK] += 1; + } + return ret; +} + +function summUpEnemyObject(obj, stat) { + var ret = classifyObject(obj); + var w = obj.cost; + if (obj.type === STRUCTURE) { + for (var scope in SCOPES) + if (canReachBy(scope, obj)) // structures don't move, so they are usually counted as defense + addStat(stat.defense[scope], ret, w); + if (obj.range > baseScale * 2) // unless they're covering the whole map with their range + addStat(stat.offense, ret, w); + } + if (obj.type === DROID) { + for (var scope in SCOPES) + if (canReachBy(scope, obj)) // if the droid can't reach your base, we count it as defense only + addStat(stat.defense[scope], ret, w); + if (threatensBase(obj)) // otherwise count them as offense as well + addStat(stat.offense, ret, w); + } +} + +function summUpMyObject(obj, stat) { + var ret = classifyObject(obj); + var w = obj.cost; + if (obj.type === STRUCTURE) { + addStat(stat.defense, ret, w); + if (obj.range > baseScale * 2) + for (var scope in SCOPES) + addStat(stat.offense[scope], ret, w); + } + if (obj.type === DROID) { + if (obj.group === miscGroup) + addStat(stat.defense, ret, w); + var list = enumLivingPlayers(); + list.forEach(function(p) { + if (isEnemy(p)) { + if (countLandTargets(p) > 0) + addStat(stat.offense.land, ret, w / list.length); + if (countSeaTargets(p) > 0) + addStat(stat.offense.sea, ret, w / list.length); + if (countAirTargets(p) > 0) + addStat(stat.offense.air, ret, w / list.length); + } + }); + } +} + +_global.adaptCycle = function() { + if (!defined(adaptCycle.player)) + adaptCycle.player = me; + if (!defined(adaptCycle.phase)) + adaptCycle.phase = 0; + if (stack.length > 0) { + var items = MAX_PER_CYCLE; + if (items > stack.length) + items = stack.length; + for (var i = 0; i < items; ++i) { + var obj = stack.pop(); + if (isEnemy(adaptCycle.player)) + summUpEnemyObject(obj, enemyStatsTemp[adaptCycle.player]); + else if (adaptCycle.player === me) + summUpMyObject(obj, myStatsTemp); + } + return; + } + ++adaptCycle.phase; + switch(adaptCycle.phase) { + case 1: + stack = enumStruct(adaptCycle.player, DEFENSE); + return; + case 2: + stack = enumDroid(adaptCycle.player, DROID_WEAPON); + return; + case 3: + stack = enumDroid(adaptCycle.player, DROID_CYBORG); + return; + } + if (adaptCycle.player === me) { + myStats = myStatsTemp; + myStatsTemp = new MyStat; + } else { + enemyStats[adaptCycle.player] = enemyStatsTemp[adaptCycle.player]; + enemyStatsTemp[adaptCycle.player] = new EnemyStat; + } + do { + ++adaptCycle.player; + if (adaptCycle.player >= maxPlayers) + adaptCycle.player = 0; + } while (adaptCycle.player !== me && !isEnemy(adaptCycle.player)); + adaptCycle.phase = 0; +} + +function getMyGroupInfo(gr) { + var ret = new MyStat(); + enumGroup(gr).forEach(function(obj) { summUpMyObject(obj, ret); }); + return ret; +} + +// +// here be functions for querying statistics gathered above +// and making adaptive decisions +// + +function groupOurs(gr) { + function uncached() { + if (defined(gr)) + return getMyGroupInfo(gr).collapse(); + else + return myStats.collapse(); + } + return cached(uncached, 500, gr); +} + +function groupAttackOurs(gr) { + function uncached() { + if (defined(gr)) + return getMyGroupInfo(gr).collapse(); + else + return myStats.offense.collapse(); + } + return cached(uncached, 500, gr); +} + +function enemyOffense() { + var theirs = new SimpleStat(); + enumLivingPlayers().filter(isEnemy).forEach(function(p) { + addStat(theirs, enemyStats[p].offense); + }); + return theirs; +} + +function groupTheirs(gr) { + function uncached() { + if (defined(gr)) { + if (gr === miscGroup) + return enemyOffense(); + else + return enemyStats[gr].collapse(); + } else { + var theirs = new SimpleStat(); + enumLivingPlayers().filter(isEnemy).forEach(function(p) { + addStat(theirs, enemyStats[p].collapse()); + }); + return theirs; + } + } + return cached(uncached, 500, gr); +} + +// use this for research; 15% smooth +_global.chooseWeaponRole = function(gr) { + function uncached() { + if (withChance(20)) + return randomUnitArray(4); + return adaptVote(groupOurs(gr).role, groupTheirs(gr).obj, true); + } + return cached(uncached, 5000, gr); +} + +// use this for droids and long-range arty emplacements +_global.chooseAttackWeaponRole = function(gr) { + function uncached() { + return adaptVote(groupAttackOurs(gr).role, groupTheirs(gr).obj, true); + } + return cached(uncached, 5000, gr); +} + +// use this for defenses; 15% smooth +_global.chooseDefendWeaponRole = function() { + function uncached() { + if (withChance(20)) + return randomUnitArray(4); + return adaptVote(myStats.defense.role, enemyOffense().obj, true); + } + return cached(uncached, 5000); +} + +_global.chooseBodyClass = function(gr) { + function uncached() { + var our = groupAttackOurs(gr), their = groupTheirs(gr); + return adaptVote( + [ our.kbody, our.tbody ], + [ their.kweapon, their.tweapon ] + ) ? BODYCLASS.THERMAL : BODYCLASS.KINETIC; + } + return cached(uncached, 5000, gr); +} + +_global.chooseObjectType = function() { + function uncached() { + var our = groupOurs(), their = groupTheirs(); + // behaviour specific for a turtle AI + if (personality.defensiveness === 100) + if (iHaveVtol() && withChance(personality.vtolness) && adaptVote( + [ our.obj[OBJTYPE.DEFS], our.obj[OBJTYPE.VTOL] ], + [ their.role[ROLE.AA], their.role[ROLE.AT] + their.role[ROLE.AP] + 2 * their.role[ROLE.AS] ] + ) === 1) + return OBJTYPE.VTOL; + else + return OBJTYPE.DEFS; + // behaviour of a generic AI + if (withChance(personality.defensiveness) && adaptVote( + [ our.obj[OBJTYPE.TANK] + our.obj[OBJTYPE.BORG] + our.obj[OBJTYPE.VTOL], our.obj[OBJTYPE.DEFS] ], + [ their.role[ROLE.AS], their.role[ROLE.AT] + their.role[ROLE.AP] + their.role[ROLE.AA] ] + ) === 1) + return OBJTYPE.DEFS; + if (iHaveVtol() && withChance(personality.vtolness) && adaptVote( + [ our.obj[OBJTYPE.TANK] + our.obj[OBJTYPE.BORG] + our.obj[OBJTYPE.DEFS], our.obj[OBJTYPE.VTOL] ], + [ their.role[ROLE.AA], their.role[ROLE.AT] + their.role[ROLE.AP] + their.role[ROLE.AS] ] + ) === 1) + return OBJTYPE.VTOL; + return adaptVote( + [ our.obj[OBJTYPE.TANK], our.obj[OBJTYPE.BORG] ], + [ their.role[ROLE.AP], their.role[ROLE.AT] ] + ) ? OBJTYPE.BORG : OBJTYPE.TANK; + } + return cached(uncached, 5000); +} + +_global.scopeRatings = function() { + function uncached() { + var ret = { land: 0, sea: 0, air: 0 }; + enumLivingPlayers().filter(isEnemy).forEach(function(player) { + ret.land += countLandTargets(player); + ret.sea += countSeaTargets(player); + ret.air += countAirTargets(player); + }); + return ret; + } + return cached(uncached, 5000); +} + +_global.spendMoney = function() { + queue("checkResearch", 100); + if (checkTruckProduction()) + return; // will proceed on the next cycle + if (chooseObjectType() == OBJTYPE.DEFS) + queue("checkConstruction", 200); + else + queue("checkProduction", 300); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/build.js b/har_includes-custom/build.js new file mode 100644 index 0000000..e506eab --- /dev/null +++ b/har_includes-custom/build.js @@ -0,0 +1,368 @@ + +/* + * This file describes building construction procedures. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +function randomLocation() { + var x = baseLocation.x + random(baseScale) - baseScale / 2; + var y = baseLocation.y + random(baseScale) - baseScale / 2; + if (x < 3 || y < 3 || x > mapWidth - 4 || y > mapHeight - 4) + return baseLocation; + return {x: x, y: y}; + +} + +// a function for checking the presence of enemy units at the construction site +function safeSpot(x, y) { + return dangerLevel({x: x, y: y}) <= 0; +} + +function truckFree(truck) { + if (truck.droidType !== DROID_CONSTRUCT) + return false; + if (truck.order === DORDER_BUILD) + return false; + if (truck.order === DORDER_HELPBUILD) + return false; + if (truck.order === DORDER_LINEBUILD) + return false; + if (truck.order === DORDER_DEMOLISH) + return false; + return true; +} + +// returns one or two free trucks +function getTwoFreeTrucks() { + var trucks = enumTrucks().filter(truckFree); + if (trucks.length > 2) { + var ret = naiveFindClusters(trucks, baseScale / 2); + if (ret.maxCount >= 2) + trucks = ret.clusters[ret.maxIdx]; + } + if (trucks.length > 2) + trucks.length = 2; + return trucks; +} + +function getFreeTruckAround(x, y) { + var list = enumTrucks().filter(truckFree).filter(function(droid) { + return droidCanReach(droid, x, y); + }).sort(function(one, two) { + return distance(one, x, y) - distance(two, x, y); + }); + if (list.length > 0) + return list[0]; +} + +function buildModule(struct) { + trucks = getTwoFreeTrucks(); + if (trucks.length <= 0) + return BUILDRET.FAILURE; + var moduleInfo = modules.filter(function(item) { return isAvailable(item.module) && item.base === struct.stattype; }).last(); + if (!defined(moduleInfo)) + return BUILDRET.UNAVAILABLE; + if (struct.modules >= moduleInfo.count) + return BUILDRET.UNAVAILABLE; + var success = false; + for (var i = 0; i < trucks.length; ++i) + success = orderDroidBuild(trucks[i], DORDER_BUILD, moduleInfo.module, struct.x, struct.y) || success; + if (success) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function buildBasicStructure(statlist, importance) { + if (throttled(5000, statlist[0])) return BUILDRET.FAILURE; + // by default, don't try building things in dangerous locations + if (!defined(importance)) + importance = IMPORTANCE.PEACETIME; + trucks = getTwoFreeTrucks(); + if (trucks.length <= 0) + return BUILDRET.FAILURE; + // choose structure type (out of the statlist), + // together with suitable location + var idx, loc, avail = false; + for (var i = 0; i < statlist.length; ++i) + if (isAvailable(statlist[i])) { + avail = true; + if (distanceToBase(trucks[0]) <= baseScale) + loc = pickStructLocation(trucks[0], statlist[i], trucks[0].x, trucks[0].y); + else { + var rndLoc = randomLocation(); + loc = pickStructLocation(trucks[0], statlist[i],rndLoc.x, rndLoc.y); + } + idx = i; + break; + } + if (!avail) + return BUILDRET.UNAVAILABLE; + if (!defined(loc)) + return BUILDRET.FAILURE; + if (importance === IMPORTANCE.PEACETIME && !safeSpot(loc.x, loc.y)) + return BUILDRET.FAILURE; + // now actually build + var success = false; + for (var i = 0; i < trucks.length; ++i) + success = orderDroidBuild(trucks[i], DORDER_BUILD, statlist[idx], loc.x, loc.y) || success; + if (success) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function finishStructures() { + var success = false; + var list = enumStruct(me).filterProperty("status", BEING_BUILT); + for (var i = 0; i < list.length; ++i) { + if (success) + return; + if (throttled(10000, list[i].id)) + return; + if (list[i].stattype === RESOURCE_EXTRACTOR) + return; + var truck = getFreeTruckAround(list[i].x, list[i].y); + if (!defined(truck)) + return; + if (orderDroidObj(truck, DORDER_HELPBUILD, list[i])) + success = true; + }; + return success; +} + +function buildStructureAround(statlist, loc, unique) { + if (!defined(statlist)) + return BUILDRET.UNAVAILABLE; + var truck = getFreeTruckAround(loc.x, loc.y); + if (!defined(truck)) + return BUILDRET.FAILURE; + var stat = statlist.filter(isAvailable).filter(function(s) { + if (unique !== true) + return true; + var list = enumStruct(me, s); + for (var i = 0; i < list.length; ++i) + if (distance(list[i], loc) < baseScale / 2) + return false; + return true; + }).last(); + if (!defined(stat)) + return BUILDRET.UNAVAILABLE; + var loc2 = pickStructLocation(truck, stat, loc.x, loc.y); + if (!defined(loc2)) + return BUILDRET.FAILURE; + // if we're not into turtling, don't build too many towers + if (personality.defensiveness < 100 && distance(loc2, loc) > baseScale / 5) + return BUILDRET.FAILURE; + if (orderDroidBuild(truck, DORDER_BUILD, stat, loc2.x, loc2.y)) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function captureOil(oil) { + if (!defined(oil)) + return BUILDRET.FAILURE; + var truck = getFreeTruckAround(oil.x, oil.y); + if (!defined(truck)) + return BUILDRET.FAILURE; + var stat = structures.derricks.filter(isAvailable).last(); + if (!defined(stat)) + return BUILDRET.UNAVAILABLE; + if (throttled(90000, oil.y * mapWidth + oil.x)) + return BUILDRET.FAILURE; + if (orderDroidBuild(truck, DORDER_BUILD, stat, oil.x, oil.y)) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function chooseDefense(defrole) { + return weaponStatsToDefenses(chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseDefendWeaponRole(), 2, defrole), defrole); +} + +function buildTowers() { + var oils = enumStructList(structures.derricks); + if (oils.length === 0) + return false; + if (withChance(70)) + return buildStructureAround(chooseDefense(DEFROLE.STANDALONE), oils.random()) !== BUILDRET.UNAVAILABLE; + return buildStructureAround(chooseDefense(DEFROLE.FORTRESS).concat(structures.sensors), oils.random(), true) !== BUILDRET.UNAVAILABLE; +} + +function buildGateways() { + var oils = countStructList(structures.derricks); + if (oils <= 0) + return BUILDRET.FAILURE; + var gates = enumGateways().filter(function(gate) { + var l = gate.x1 - gate.x2 + gate.y1 - gate.y2; + if (l < 0) + l = -l; + var cnt = enumRange(gate.x1, gate.y1, l, ALLIES).filterProperty("stattype", DEFENSE).length; + cnt += enumRange(gate.x2, gate.y2, l, ALLIES).filterProperty("stattype", DEFENSE).length; + cnt -= enumRange(gate.x1, gate.y1, l, ENEMIES).filterProperty("stattype", DEFENSE).length; + cnt -= enumRange(gate.x2, gate.y2, l, ENEMIES).filterProperty("stattype", DEFENSE).length; + return cnt >= 0 && (cnt < l || (personality.defensiveness === 100 && withChance(70))); // turtle AI needs to keep building towers + }).sort(function(one, two) { return distanceToBase({x: one.x1, y: one.y1}) - distanceToBase({x: two.x1, y: two.y1}); }); + if (gates.length === 0) + return; + if (withChance(50)) + return buildStructureAround(chooseDefense(DEFROLE.GATEWAY), {x: gates[0].x1, y: gates[0].y1}) !== BUILDRET.UNAVAILABLE; + else + return buildStructureAround(chooseDefense(DEFROLE.GATEWAY), {x: gates[0].x2, y: gates[0].y2}) !== BUILDRET.UNAVAILABLE; +} + +function buildArty() { + return buildBasicStructure(chooseDefense(DEFROLE.ARTY)); +} + +_global.buildMinimum = function(statlist, count, importance) { + if (countStructList(statlist) < count) + if (buildBasicStructure(statlist, importance) !== BUILDRET.UNAVAILABLE) + return true; + return false; +} + +function captureSomeOil() { + if (throttled(500)) + return true; + function getOilList() { + var oils = []; + oilResources.forEach(function(stat) { oils = oils.concat(enumFeature(-1, stat)); }); + oils = oils.concat(enumStructList(structures.derricks).filterProperty("status", BEING_BUILT)); + oils = oils.sort(function(one, two) { + return distanceToBase(one) - distanceToBase(two); + }); + if (oils.length > 10) + oils.length = 10; + return oils; + } + var oils = cached(getOilList, 5000); + if (countFinishedStructList(structures.derricks) >= 4 * structListLimit(structures.gens)) + return false; + + for (var i = 0; i < oils.length; ++i) + if (captureOil(oils[i]) === BUILDRET.SUCCESS) + return true; + return false; +} + +_global.buildMinimumDerricks = function(count) { + if (countFinishedStructList(structures.derricks) < count) + if (captureSomeOil()) + return true; + return false; +} + +function buildExpand() { + if (myPower() > personality.maxPower) { + switch (chooseObjectType()) { + case 0: + if (buildMinimum(structures.factories, Infinity)) return true; + case 1: + if (buildMinimum(structures.templateFactories, Infinity)) return true; + case 3: + if (buildMinimum(structures.vtolFactories, Infinity)) return true; + } + } + return false; +} + +function buildEnergy() { + var oils = countFinishedStructList(structures.derricks); + var gens = countStructList(structures.gens); + if (oils > 4 * gens) + if (buildBasicStructure(structures.gens) !== BUILDRET.UNAVAILABLE) + return true; + if (withChance(50)) + if (captureSomeOil()) + return true; + return false; +} + +function buildModules() { + var str = []; + for (var i = 0; i < modules.length; ++i) { + str = enumStruct(me, modules[i].base); + for (var j = 0; j < str.length; ++j) + if (buildModule(str[j]) !== BUILDRET.UNAVAILABLE) + return true; + } + return false; +} + +_global.buildVtols = function() { + if (buildMinimum(structures.vtolPads, enumDroid(me, DROID_WEAPON).filter(isVTOL).length / 2)) + return true; + return false; +} + +function buildExtras() { + if (throttled(180000)) + return false; + if (buildBasicStructure(structures.extras) !== BUILDRET.UNAVAILABLE) + return true; + return false; +} + +function buildDefenses() { + if (chooseObjectType() !== 2) + return false; + if (withChance(33)) { + if (buildTowers()) // includes sensor towers and forts + return true; + } else if (withChance(50)) { + if (buildGateways()) + return true; + } else + if (buildArty()) + return true; + return false; +} + +function listOutdatedDefenses() { + for (var path in weaponStats) { + for (var role in DEFROLE) { + var list = weaponStatsToDefenses(weaponStats[path], DEFROLE[role]); + for (var i = 0; i < list.length - 2; ++i) + if (isAvailable(list[i + 2])) { + if (countStruct(list[i]) > 0) + return enumStruct(me, list[i]); + } + } + }; + return []; +} + +function recycleDefenses() { + var trucks = enumTrucks().filter(truckFree); + if (trucks.length <= 0) + return false; + var list = listOutdatedDefenses(); + for (var i = 0; i < list.length; ++i) + for (var j = 0; j < trucks.length; ++j) + if (droidCanReach(trucks[j], list[i].x, list[i].y)) { + orderDroidObj(trucks[j], DORDER_DEMOLISH, list[i]); + return true; + } + return false; +} + +_global.checkConstruction = function() { + if (enumTrucks().filter(truckFree).length === 0) + return; + if (functionSeries("construction", [ + finishStructures, + buildOrder, + buildExpand, + buildEnergy, + buildModules, + buildVtols, + buildExtras, + recycleDefenses, + buildDefenses, + ])) + queue("checkConstruction"); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/chat.js b/har_includes-custom/chat.js new file mode 100644 index 0000000..2fb290a --- /dev/null +++ b/har_includes-custom/chat.js @@ -0,0 +1,151 @@ +/* + * This file is responsible for chat listening. It contains handler functions for different chat + * messages; each such function takes message and player as a parameter and returns a reply string. + * All chat talking (such as calling for help) is coded in other places, not here. + * + * Messages are marked as translatable when necessary. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +var prefix = '!har'; + +// key: name in chat, value: function that will be executed +// function gets two params: sender and argument +var commandMap = { + set: chatSet, + res: chatRes, + truck: chatTruck, + power: chatMoney, + money: chatMoney, // alias for "power" + help: chatHelp, + go: chatHelp, // alias for "help" + tx: chatUnhelp, +} + +var beaconInfo = []; + +_global.noticeBeacon = function(x, y, from) { + beaconInfo[from] = { + x: x, + y: y, + exists: 1, + }; +} + +_global.unnoticeBeacon = function(from) { + beaconInfo[from].exists = 0; +} + +_global.findBeaconPlayer = function(x, y) { + for (var i = 0; i < beaconInfo.length; ++i) + if (defined(beaconInfo[i]) && beaconInfo[i].x === x && beaconInfo[i].y === y) + return i; +} + +_global.handleChatMessage = function(sender, receiver, message) { + // don't reply on any other message coming sender enemies + if (message === "!har who") { + chat(sender, chatWho(sender)); + return; + } + if (!isAlly(sender)) + return; + if (message === "help me!!") { // Try to understand Nexus AI's way of calling for help + chatHelp(sender); + return; + } + var result = message.split(/ +/); + if (result[0] !== prefix) + return; + var command = result[1]; + var argument = result[2]; + if (defined(commandMap[command])) + chat(sender, commandMap[command](sender, argument)); +} + +function chatWho(sender, argument) { + var str = "NullBot3 (" + scriptName + ") "; + switch(difficulty) { + case EASY: str += _("EASY"); break; + case MEDIUM: str=str + _("MEDIUM"); break; + case HARD: str=str + _("HARD"); break; + case INSANE: str=str + _("INSANE"); break; + } + if (isAlly(sender)) + str += (" ~" + personality.chatalias + "~"); + return str; +} + +function chatSet(sender, argument) { + var str = ""; + for (var i in subpersonalities) { + if (subpersonalities[i].chatalias === argument) { + personality = subpersonalities[i]; + return _("Personality change successful."); + } + str = str + " " + subpersonalities[i].chatalias; + } + return _("No such personality! Try one of these:") + str; +} + +function chatRes(sender, argument) { + if (argument === "cl") { + setForcedResearch(); // clear + return _("Forced research cleared, will research anything I want now."); + } + if (argument === "no") { + setForcedResearch(null); // clear + return _("Research blocked, will research nothing now."); + } + if (argument === "fn") { + setForcedResearch(fundamentalResearch); + return _("Researching fundamental technology."); + } + var str = " cl no fn"; + for (var i in weaponStats) { + if (weaponStats[i].chatalias === argument) { + setForcedResearch(weaponStatsToResList(weaponStats[i])); + return _("Researching ") + weaponStats[i].chatalias; + } + if (weaponStats[i].chatalias.indexOf("useless") < 0) + str = str + " " + weaponStats[i].chatalias; + } + return _("No such research path! Try one of these:") + str; +} + +function chatTruck(sender, argument) { + var droid = enumTrucks().random(); + if (!defined(droid)) + return _("Sorry, I have no trucks."); + if (donateObject(droid, sender)) { + addBeacon(droid.x, droid.y, sender); + return _("You can use this one."); + } + return _("Sorry, droid transfer failed."); +} + +function chatMoney(sender, argument) { + var power = Math.round(myPower()/3); + donatePower(power, sender); + return _("Power transferred."); +} + +function chatHelp(sender, argument) { + if (!defined(beaconInfo[sender]) || !beaconInfo[sender].exists) + return _("Please put a beacon!"); + if (setTarget({ x: beaconInfo[sender].x, y: beaconInfo[sender].y, type: POSITION })) + return _("Coming!"); + else + return _("Sorry, I don't have any free forces to send for help!"); +} + +function chatUnhelp(sender, argument) { + unsetTarget(sender); + return _("Any time, big boss!"); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/intensity.js b/har_includes-custom/intensity.js new file mode 100644 index 0000000..41241b7 --- /dev/null +++ b/har_includes-custom/intensity.js @@ -0,0 +1,95 @@ + +/* + * This file includes generic functions for improving CPU intensity, + * like caching and throttling mechanisms. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// to make sure some function is not called more often than every 2 seconds, +// put something like that at start the of its body: +// if (throttled(2000)) return; +// if your function requires several throttling cases, you can use the optional +// notes value: +// if (throttled(60000, player)) return; +// NOTE: it won't work if the function repeatedly dies and gets created again, eg. +// a function defined inside forEach(function(...){...}) can't be throttled +_global.throttled = function(interval, notes) { + if (!defined(arguments.callee.caller.throttleTimes)) + arguments.callee.caller.throttleTimes = {}; + if (!defined(arguments.callee.caller.throttleTimes[notes])) { + arguments.callee.caller.throttleTimes[notes] = gameTime; + return false; + } + if (gameTime - arguments.callee.caller.throttleTimes[notes] < interval) + return true; + arguments.callee.caller.throttleTimes[notes] = gameTime; + return false; +} + +// to cache a function's output value and make sure it's not re-calculated too often, +// use the following trick: +// function calculateValue(params) { +// function uncached(params) { +// // do heavy calculations +// } +// return cached(uncached, 10000); +// } +// add necessary notes to the cached() call if necessary, similar to how you do it +// for throttled(). +// NOTE: it won't work if the function repeatedly dies and gets created again, eg. +// a function defined inside forEach(function(...){...}) can't have caching inside +_global.cached = function(whatToCall, interval, notes) { + if (!defined(arguments.callee.caller.cachedTimes)) { + arguments.callee.caller.cachedTimes = {}; + arguments.callee.caller.cachedValues = {}; + } + var t = arguments.callee.caller.cachedTimes[notes]; + if (!defined(t) || gameTime - t >= interval) { + arguments.callee.caller.cachedValues[notes] = whatToCall(); + arguments.callee.caller.cachedTimes[notes] = gameTime; + } + return arguments.callee.caller.cachedValues[notes]; +} + +// if you actually want your script to send debug messages, consider using this function. +// it will only output each message only once, so your debug log will be readable. +_global.niceDebug = function() { + var msg = me + ": " + Array.prototype.join.call(arguments, " "); + if (throttled(Infinity, msg)) + return; + debug(msg); +} + +// use this if you want to split a certain void function into parts. +// example: +// function first() { return random(2); } +// function second() { return random(2); } +// function third() { return random(2); } +// function doManyThings() { +// if (functionSeries("doStuff", [ first, second, third ])) +// queue("doManyThings"); +// } +// NOTE: in this example functions of the list don't need to be global, but +// doManyThings does, otherwise the queue call will fail. +_global.functionSeries = function(id, list) { + if (!defined(functionSeries.last)) + functionSeries.last = {}; + if (!defined(functionSeries.last[id])) + functionSeries.last[id] = 0; + else + ++functionSeries.last[id]; + if (functionSeries.last[id] >= list.length) { + functionSeries.last[id] = 0; + return false; // all functions in the list were called, none succeeded + } + if (!list[functionSeries.last[id]]()) + return true; // none of the function succeeded yet, telling to call us again next time + functionSeries.last[id] = undefined; + return false; // one of the functions succeeded, no need to call us anymore +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/lassat.js b/har_includes-custom/lassat.js new file mode 100644 index 0000000..6e5b015 --- /dev/null +++ b/har_includes-custom/lassat.js @@ -0,0 +1,30 @@ + +/* + * This file is responsible for incoming laser satellite strikes. + * Relies on lassatSplash variable of the ruleset to figure out + * the radius of lassat. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// pick a target and fire +_global.fireLassat = function(structure) { + list = []; + enumLivingPlayers().filter(isEnemy).forEach(function(i) { + list = list.concat(enumStruct(i), enumDroid(i)); + }); + var maxIdx, maxPrice = 0; + list.forEach(function(obj, idx) { + var price = enumRange(obj.x, obj.y, lassatSplash / 2, ENEMIES, false).reduce(function(prev, curr) { return prev + curr.cost; }, 0); + if (price > maxPrice) { + maxPrice = price; + maxIdx = idx; + } + }); + activateStructure(structure, list[maxIdx]); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/math.js b/har_includes-custom/math.js new file mode 100644 index 0000000..169398e --- /dev/null +++ b/har_includes-custom/math.js @@ -0,0 +1,149 @@ + +/* + * This file includes routines and algorithms completely unrelated to Warzone 2100. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// return random true or false in certain chance +// 0 will return true with chance 0% +// 50 will return true with chance 50% +// 100 will return true with chance 100% +_global.withChance = function distance(chancePercent) { + return 1 + random(100) <= chancePercent; +} + +// Get distance between two points +// acceptable arguments: +// distance(obj, obj) +// distance(x,y, obj) +// distance(obj,x,y) +_global.distance = function distance(obj1, obj2, obj3, obj4) { + var x1, x2, y1, y2; + if (defined(obj1.x)) { + x1 = obj1.x; + y1 = obj1.y; + if (defined(obj2.x)) { + x2 = obj2.x; + y2 = obj2.y; + } else { + x2 = obj2; + y2 = obj3; + } + } else { + x1 = obj1; + y1 = obj2; + if (defined(obj3.x)) { + x2 = obj3.x; + y2 = obj3.y; + } else { + x2 = obj3; + y2 = obj4; + } + } + return distBetweenTwoPoints(x1, y1, x2, y2); +} + +_global.defined = function(variable) { + return typeof(variable) !== "undefined"; +} + +// returns a random number between 0 and max-1 inclusively +_global.random = function(max) { + if (max > 0) + return Math.floor(Math.random() * max); +} + +// some useful array functions +Array.prototype.random = function() { + if (this.length > 0) + return this[random(this.length)]; +} + +Array.prototype.last = function() { + if (this.length > 0) + return this[this.length - 1]; +} + +Array.prototype.filterProperty = function(property, value) { + return this.filter(function(element) { + return element[property] === value; + }); +} + +Array.prototype.someProperty = function(property, value) { + return this.some(function(element) { + return element[property] === value; + }); +} + +Array.prototype.shuffle = function() { + return this.sort(function() { return 0.5 - Math.random(); }) +} + +_global.zeroArray = function(l) { + var ret = []; + for (var i = 0; i < l; ++i) + ret[i] = 0; + return ret; +} + +_global.randomUnitArray = function(l) { + var ret = zeroArray(l); + ret[random(l)] = 1; + return ret; +} + +Array.prototype.addArray = function(arr) { + for (var i = 0; i < this.length; ++i) + this[i] += arr[i]; +} + +// returns a random property of an object +_global.randomItem = function(obj) { + var ret; + var count = 0; + for (var i in obj) + if (Math.random() < 1/++count) + ret = i; + return obj[ret]; +} + +// cluster analysis happens here +_global.naiveFindClusters = function(list, size) { + var ret = { clusters: [], xav: [], yav: [], maxIdx: 0, maxCount: 0 }; + for (var i = list.length - 1; i >= 0; --i) { + var x = list[i].x, y = list[i].y; + var found = false; + for (var j = 0; j < ret.clusters.length; ++j) { + if (distance(ret.xav[j], ret.yav[j], x, y) < size) { + var n = ret.clusters[j].length; + ret.clusters[j][n] = list[i]; + ret.xav[j] = (n * ret.xav[j] + x) / (n + 1); + ret.yav[j] = (n * ret.yav[j] + y) / (n + 1); + if (ret.clusters[j].length > ret.maxCount) { + ret.maxIdx = j; + ret.maxCount = ret.clusters[j].length; + } + found = true; + break; + } + } + if (!found) { + var n = ret.clusters.length; + ret.clusters[n] = [list[i]]; + ret.xav[n] = x; + ret.yav[n] = y; + if (1 > ret.maxCount) { + ret.maxIdx = n; + ret.maxCount = 1 + } + } + } + return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/misc.js b/har_includes-custom/misc.js new file mode 100644 index 0000000..b5fbf87 --- /dev/null +++ b/har_includes-custom/misc.js @@ -0,0 +1,56 @@ + +/* + * A few globals that didn't have a place for them in any other file + * + */ + +var baseLocation = startPositions[me]; + +function distanceToBase(loc) { + return distance(loc, baseLocation); +} + +function canReachFromBase(propulsion, location) { + if (defined(propulsion)) + return propulsionCanReach(propulsion, baseLocation.x, baseLocation.y, location.x, location.y); +} + +function myPower() { + return playerPower(me) - queuedPower(me); +} + +function isAvailable(stat) { + return isStructureAvailable(stat, me); +} + +function isAlly(player) { + return allianceExistsBetween(me, player); +} + +function isEnemy(player) { + return !isAlly(player); +} + +function enumLivingPlayers() { + function uncached() { + var ret = []; + for (var i = 0; i < maxPlayers; ++i) { + if (countStructList(targets, i) > 0) { + ret.push(i); + continue; + } + if (enumDroid(i).length > 0) + ret.push(i); + } + return ret; + }; + return cached(uncached, 30000); +} + +function enumTrucks() { + return enumDroid(me, DROID_CONSTRUCT); +} + +function goEasy() { + personality = randomItem(subpersonalities); +} diff --git a/har_includes-custom/produce.js b/har_includes-custom/produce.js new file mode 100644 index 0000000..c2638c7 --- /dev/null +++ b/har_includes-custom/produce.js @@ -0,0 +1,239 @@ + +/* + * This file is responsible for droid production. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +function ourBuildDroid(factory, name, bodies, propulsions, weapons1, weapons2, weapons3) { + return buildDroid(factory, name, bodies, propulsions, "", "", weapons1, weapons2, weapons3); +} + +function produceTruck(factory, turrets) { + var turret = truckTurrets.concat(); + if (defined(turrets)) + turret = turrets.concat(); + turret.reverse(); + // TODO: switch to using chooseBodyWeaponPair() here + var bodies = filterBodyStatsByUsage(BODYUSAGE.TRUCK, BODYCLASS.KINETIC).map(function(val) { return val.stat; }); + var propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND|PROPULSIONUSAGE.HOVER); + return ourBuildDroid(factory, "Fancy Truck", bodies, propulsions, turret); +} + +function chooseWeapon(forVtol) { + if (!defined(forVtol)) + forVtol = false; + if (forVtol) { + var ret = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 3); + if (defined(ret)) + return ret.vtols.concat().reverse(); + } else { + var ret = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 0); + if (defined(ret)) + return ret.weapons.concat().reverse(); + } +} + +function chooseBodyWeaponPair(bodies, weapons) { + if (!defined(bodies)) + return undefined; + if (!defined(weapons)) + return undefined; + for (var i = 0; i < weapons.length; ++i) { + var w = weapons[i].stat, ww = weapons[i].weight; + if (!componentAvailable(w)) + continue; + for (var j = 0; j < bodies.length; ++j) { + var b = bodies[j].stat, bw = bodies[j].weight; + if (!componentAvailable(b)) + continue; + switch(ww) { + case WEIGHT.ULTRALIGHT: + if (bw <= WEIGHT.LIGHT) + return {b: b, w: w}; + break; + case WEIGHT.LIGHT: + if (bw <= WEIGHT.MEDIUM) + return {b: b, w: w}; + break; + case WEIGHT.MEDIUM: + return {b: b, w: w}; + break; + case WEIGHT.HEAVY: + if (bw >= WEIGHT.MEDIUM) + return {b: b, w: w}; + break; + case WEIGHT.ULTRAHEAVY: + if (bw >= WEIGHT.HEAVY) + return {b: b, w: w}; + break; + } + } + } +} + +function produceTank(factory) { + // TODO: needs refactoring. Make some more clever sorting. + var bodies = [] + if (chooseBodyClass() === BODYCLASS.KINETIC) { + bodies = bodies.concat( + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.KINETIC), + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.THERMAL) + ); + } else { + bodies = bodies.concat( + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.THERMAL), + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.KINETIC) + ); + } + var propulsions; + var ret = scopeRatings(); + var rnd = random(ret.land + ret.sea); + if (!defined(rnd)) // we need only vtols? + return false; + propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND); + if (iHaveHover()) { + if (rnd >= ret.land) + propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER); + } else { + if (ret.land === 0) + return false; + } + var bwPair = chooseBodyWeaponPair(bodies, chooseWeapon()); + if (!defined(bwPair)) + return false; + return ourBuildDroid(factory, "Tank", bwPair.b, propulsions, bwPair.w, bwPair.w, bwPair.w); +} + +function produceVtol(factory) { + // TODO: consider thermal bodies + var bodies = filterBodyStatsByUsage(BODYUSAGE.AIR, BODYCLASS.KINETIC) + var propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.VTOL); + var bwPair = chooseBodyWeaponPair(bodies, chooseWeapon(true)); + if (!defined(bwPair)) + return false; + return ourBuildDroid(factory, "VTOL", bwPair.b, propulsions, bwPair.w, bwPair.w, bwPair.w); +} + +function produceTemplateFromList(factory, list) { + var ret = scopeRatings(); + for (var i = list.length - 1; i >= 0; --i) { + if (ret.land === 0 && !isHoverPropulsion(list[i].prop) && !isVtolPropulsion(list[i].prop)) + continue; + if (ret.land === 0 && ret.sea === 0 && !isVtolPropulsion(list[i].prop)) + continue; + if (isVtolPropulsion(list[i].prop) !== (factory.stattype === VTOL_FACTORY)) + continue; + if ((!randomTemplates) || withChance(100 / (i + 1))) + if (ourBuildDroid(factory, "Template Droid", list[i].body, list[i].prop, list[i].weapons[0], list[i].weapons[1], list[i].weapons[2])) + return true; + } + return false; +} + +function produceTemplate(factory) { + var path = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 1); + if (defined(path)) + return produceTemplateFromList(factory, path.templates); + return false; +} + +_global.checkTruckProduction = function() { + var trucks = enumTrucks(); + var hoverTrucksCount = trucks.filter(function(droid) { return isHoverPropulsion(droid.propulsion); }).length; + if (iHaveHover() && hoverTrucksCount < personality.minHoverTrucks) { + groundTrucks = trucks.filter(function(droid) { return !isHoverPropulsion(droid.propulsion); }); + if (groundTrucks.length > personality.minTrucks) { + groundTrucks.length -= personality.minTrucks; + groundTrucks.forEach(function(droid) { orderDroid(droid, DORDER_RECYCLE); }); + return false; + } + } + if (trucks.length >= getDroidLimit(me, DROID_CONSTRUCT)) + return false; + if (trucks.length < personality.minTrucks || myPower() > personality.maxPower + || (iHaveHover() && hoverTrucksCount < personality.minHoverTrucks) + ) { + var f; + f = enumFinishedStructList(structures.factories)[0]; + if (defined(f)) + if (structureIdle(f)) + if (produceTruck(f)) + return true; + if (defined(f)) + return false; + f = enumFinishedStructList(structures.templateFactories)[0]; + if (defined(f)) + if (structureIdle(f)) + if (produceTemplateFromList(f, truckTemplates)) + return true; + } + if (!iHaveArty()) + return false; + var sensors = enumDroid(me, DROID_SENSOR).length; + if (withChance(100 - 100 * sensors / personality.maxSensors)) { + f = enumFinishedStructList(structures.factories)[0]; + if (defined(f)) + if (structureIdle(f)) + if (produceTruck(f, sensorTurrets)) + return true; + } + return false; +} + +function checkTankProduction() { + if (!iCanDesign()) + return false; // don't cheat by producing tanks before design is available (also saves money for early generators) + var success = false; + enumIdleStructList(structures.factories).forEach(function(factory) { + success = success || produceTank(factory); + }); + return success; +} + +function checkTemplateProduction() { + var success = false; + enumIdleStructList(structures.templateFactories) + .concat(enumIdleStructList(structures.vtolFactories)) + .forEach(function(factory) + { + success = success || produceTemplate(factory); + }); + return success; +} + +function checkVtolProduction() { + var success = false; + if (!iCanDesign()) + return false; // don't cheat by producing vtols before design is available + enumIdleStructList(structures.vtolFactories).forEach(function(factory) { + success = success || produceVtol(factory); + }); + return success; +} + +_global.checkProduction = function() { + switch(chooseObjectType()) { + case 1: + if (checkTemplateProduction()) + return; + case 3: + if (checkVtolProduction()) + return; + default: + if (checkTankProduction()) + return; + } + // if having too much energy, don't care about what we produce + if (myPower() > personality.maxPower) { + queue("checkConstruction"); + checkTemplateProduction(); + checkTankProduction(); + checkVtolProduction(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/research.js b/har_includes-custom/research.js new file mode 100644 index 0000000..08f8e4a --- /dev/null +++ b/har_includes-custom/research.js @@ -0,0 +1,78 @@ + +/* + * This file controls the AI's research choices. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +var forcedResearch; + +_global.setForcedResearch = function(list) { + forcedResearch = list; +} + +function doResearch(lab) { + if (defined(forcedResearch)) { + if (forcedResearch === null) + return false; + if (pursueResearch(lab, forcedResearch)) + return true; + } + // if we need to quickly get a certain propulsion to reach the enemy, prioritize that. + var ret = scopeRatings(); + if (ret.land === 0 && ret.sea === 0 && !iHaveVtol()) + if (pursueResearch(lab, [ + propulsionStatsToResList(PROPULSIONUSAGE.VTOL), + fastestResearch, + ].random())) + return true; + if (ret.land === 0 && ret.sea !== 0 && !iHaveHover() && !iHaveVtol()) + if (pursueResearch(lab, [ + propulsionStatsToResList(PROPULSIONUSAGE.HOVER), + propulsionStatsToResList(PROPULSIONUSAGE.VTOL), + fastestResearch, + ].random())) + return true; + // otherwise, start with completing the fixed research path + if (defined(personality.earlyResearch)) + if (pursueResearch(lab, personality.earlyResearch)) + return true; + // then, see if we want to research some weapons + var objType = chooseObjectType(); + if (withChance(70)) { // TODO: make a more thoughtful decision here + var list = weaponStatsToResList(chooseAvailableWeaponPathByRoleRatings(personality.weaponPaths, chooseWeaponRole()), objType); + if (pursueResearch(lab, list)) + return true; + } + if (withChance(65)) { // TODO: make a more thoughtful decision here + if (chooseBodyClass() === BODYCLASS.KINETIC) { + if (withChance(40)) + if (pursueResearch(lab, classResearch.kinetic[objType])) + return true; + if (objType === OBJTYPE.TANK || objType === OBJTYPE.VTOL || (objType === OBJTYPE.DEFS && personality.defensiveness < 100)) + if (pursueResearch(lab, bodyStatsToResList(BODYCLASS.KINETIC))) + return true; + } else { + if (withChance(40)) + if (pursueResearch(lab, classResearch.thermal[objType])) + return true; + if (objType === OBJTYPE.TANK || objType === OBJTYPE.VTOL || (objType === OBJTYPE.DEFS && personality.defensiveness < 100)) + if (pursueResearch(lab, bodyStatsToResList(BODYCLASS.THERMAL))) + return true; + } + } + // if nothing of the above holds, do some generic research + if (pursueResearch(lab, fundamentalResearch)) + return true; + return false; + +} + +_global.checkResearch = function() { + enumIdleStructList(structures.labs).forEach(doResearch); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/stats.js b/har_includes-custom/stats.js new file mode 100644 index 0000000..b970904 --- /dev/null +++ b/har_includes-custom/stats.js @@ -0,0 +1,233 @@ + +/* + * This file contain functions for manipulating stats defined by rulesets. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +_global.isHoverPropulsion = function(str) { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.HOVER && val.stat === str; }); +} + +_global.isVtolPropulsion = function(str) { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.VTOL && val.stat === str; }); +} + +_global.iHaveHover = function() { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.HOVER && componentAvailable(val.stat);} ); +} + +_global.iHaveVtol = function() { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.VTOL && componentAvailable(val.stat);} ); +} + +_global.iHaveArty = function() { + for (var stat in weaponStats) + for (var i = 0; i < weaponStats[stat].defenses.length; ++i) + if (weaponStats[stat].defenses[i].defrole === DEFROLE.ARTY) + if (countStruct(weaponStats[stat].defenses[i].stat) > 0) + return true; + return false; +} + +// works with stored droid objects too! +_global.safeIsVtol = function(droid) { + return isVtolPropulsion(droid.propulsion); +} + +_global.enumStructList = function(list, player) { + if (!defined(player)) + player = me; + return list.reduce(function(summ, new_value) { return summ.concat(enumStruct(player, new_value)); }, []); +} + +_global.countStructList = function(list, player) { + if (!defined(player)) + player = me; + return list.reduce(function(summ, new_value) { return summ + countStruct(new_value, player); }, 0); +} + +_global.enumFinishedStructList = function(list, player) { + return enumStructList(list, player).filterProperty("status", BUILT); +} + +_global.countFinishedStructList = function(list, player) { + return enumFinishedStructList(list, player).length; +} + +_global.enumIdleStructList = function(list, player) { + return enumFinishedStructList(list, player).filter(structureIdle); +} + +_global.structListLimit = function(list) { + return list.reduce(function (summ, val) {return summ + getStructureLimit(val)}, 0) +} + +_global.guessWeaponRole = function (name) { + for (var stat in weaponStats) { + if ( + weaponStats[stat].weapons.someProperty("stat", name) || + weaponStats[stat].vtols.someProperty("stat", name) || + weaponStats[stat].templates.some(function (i) {return i.weapons.indexOf(name) > -1;}) + ) + return weaponStats[stat].roles; + } + niceDebug("Ruleset warning: Couldn't guess weapon role of", name); +} + +function guessWeaponMicro(name) { + function uncached() { + for (var stat in weaponStats) + if (weaponStats[stat].weapons.someProperty("stat", name)) + return weaponStats[stat].micro; + if (weaponStats[stat].templates.some(function(i) { return i.weapons.indexOf(name) > -1; })) + return weaponStats[stat].micro; + } + return cached(uncached, Infinity, name); +} + +_global.guessDroidMicro = function(droid) { + for (var i = 0; i < droid.weapons.length; ++i) { + var ret = guessWeaponMicro(droid.weapons[i].name); + if (ret !== MICRO.RANGED) + return ret; + } + return MICRO.RANGED; +} + +_global.guessBodyArmor = function(name) { + var body = bodyStats.filterProperty("stat", name).last() + if (defined(body)) + return body.armor; + else + niceDebug("Ruleset warning: Couldn't guess body class of", name); +} + +function weaponPathIsAvailable(path, objectType, defrole) { + switch(objectType) { + case 0: + return path.weapons.some(function(val) { return componentAvailable(val.stat); }) + case 1: + return path.templates.some(function(val) { + for (var i = 0; i < val.weapons.length; ++i) + if (!componentAvailable(val.weapons[i])) + return false; + return componentAvailable(val.body) && componentAvailable(val.prop); + }); + case 2: + return path.defenses.some(function(val) { return val.defrole === defrole && isAvailable(val.stat); }); + case 3: + return path.vtols.some(function(val) { return componentAvailable(val.stat); }); + default: // research + return true; + } +} + +_global.getProductionPaths = function() { + if (!defined(fallbackWeapon) || gameTime > 600000) + return personality.weaponPaths; + return [weaponStats[fallbackWeapon]].concat(personality.weaponPaths); +} + +_global.chooseAvailableWeaponPathByRoleRatings = function(paths, rating, objectType, defrole) { + var minDist = Infinity, minPath; + paths.forEach(function(path) { + if (!weaponPathIsAvailable(path, objectType, defrole)) + return; + var dist = 0; + for (var i = 0; i < ROLE.LENGTH; ++i) { + var newDist = Math.abs(rating[i] - path.roles[i]) + if (newDist > dist) + dist = newDist; + } + if (dist < minDist) { + minDist = dist; + minPath = path; + } + }); + return minPath; +} + +// +// here be functions for querying out research paths +// + +function statsToResList(path) { + return path.map(function (val) { return val.res; }); +} + +// todo make one StatsToResList and do filtering for path outside +_global.bodyStatsToResList = function(armor) { + return statsToResList(filterBodyStatsByUsage(armor)).reverse(); +} + +_global.propulsionStatsToResList = function(usage) { + return statsToResList(filterDataByFlag(propulsionStats, 'usage', usage)); +} + +_global.weaponStatsToResList = function(path, objType) { + if (!defined(path)) + return []; + var ret = []; + switch(objType) { + case 0: + ret = statsToResList(path.weapons); break; + case 1: + ret = statsToResList(path.templates); break; + case 2: + ret = statsToResList(path.defenses); break; + case 3: + ret = statsToResList(path.vtols); break; + } + if (ret.length === 0) + ret = ret.concat( + statsToResList(path.weapons), + statsToResList(path.templates), + statsToResList(path.defenses), + statsToResList(path.vtols) + ); + return ret; +} + +// +// here be functions for querying out component lists +// + +// TODO: move this to math. If we have same structure of data we can use it all around. +_global.filterDataByFlag = function(data, attr_name, flag) { + return data.filter(function(obj) { return obj[attr_name] & flag; }); +} + +_global.filterBodyStatsByUsage = function(usage, armor) { + var data; + if (defined(armor)) + data = filterDataByFlag(bodyStats, 'armor', armor) + else + data = bodyStats; + return filterDataByFlag(data, 'usage', usage).reverse(); +} + +_global.getPropulsionStatsComponents = function(usage) { + var data = filterDataByFlag(propulsionStats, 'usage', usage) + return data.map(function(val) { return val.stat; }).reverse() +} + +// +// here be functions for querying out defensive structures +// + +_global.weaponStatsToDefenses = function(stats, defrole) { + if (!defined(stats)) + return []; + var ret = []; + for (var i = 0; i < stats.defenses.length; ++i) + if (!defined(defrole) || stats.defenses[i].defrole === defrole) + ret.push(stats.defenses[i].stat); + // reverse not needed here + return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/tactics.js b/har_includes-custom/tactics.js new file mode 100644 index 0000000..6d1abe7 --- /dev/null +++ b/har_includes-custom/tactics.js @@ -0,0 +1,462 @@ + +/* + * This file is responsible for moving combat unit around. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +_global.MAX_GROUPS = maxPlayers; +_global.miscGroup = MAX_GROUPS; +_global.vtolGroup = miscGroup + 1; +var groupInfo = []; +var firstTimeHarass = true; + +function GroupInfo() { + this.lastAttacked = undefined; // gameTime at the moment of the last combat +} + +function safeGetObject(label) { + var obj = getObject(label); + if (obj === null) + return undefined; + return obj; +} + +function groupsBySize() { + var ret = []; + for (var i = 0; i < MAX_GROUPS; ++i) + if (isEnemy(i)) + ret.push(i); + ret.sort(function(one, two) { return groupSize(two) - groupSize(one); }); + return ret; +} + +function findLargestGroupIn(list) { + var sizes = [] + for (var i = 0; i < MAX_GROUPS; ++i) + sizes[i] = 0; + list.forEach(function(object) { + if (!defined(object.group) || object.group < 0 && object.group >= MAX_GROUPS) + return; + ++sizes[object.group]; + }); + var maxCount = 0, maxIdx = 0; + for (var i = 0; i < sizes.length; ++i) + if (sizes[i] > maxCount) { + maxCount = sizes[i]; + maxIdx = i; + } + return maxIdx; +} + +function findNearestGroup(x, y) { + var ret = naiveFindClusters(enumDroid(me).filter(function(droid) { + return !isVTOL(droid) && (droid.droidType === DROID_WEAPON || droid.droidType === DROID_CYBORG); + }), baseScale / 3); + if (ret.maxCount === 0) + return undefined; + var minDist = Infinity, minIdx; + var gr = []; + for (var i = 0; i < ret.clusters.length; ++i) { + gr[i] = findLargestGroupIn(ret.clusters[i]); + if (groupSize(gr[i]) > attackGroupSize()) { + var dist = distance(ret.xav[i], ret.yav[i], x, y); + if (dist < minDist) { + minDist = dist; + minIdx = i; + } + } + } + if (defined(minIdx)) + return gr[minIdx]; + gr = groupsBySize(); + if (gr.length > 0) + if (groupSize(gr[0]) > attackGroupSize()) + return gr[0]; +} + +function targetSuitableForHarass(object) { + function uncached() { + var ret = enumRange(object.x, object.y, baseScale / 2, ENEMIES, false).filter(function(obj) { + return !(obj.type == STRUCTURE && obj.stattype != DEFENSE); + }).length; + return ret <= groupSize(miscGroup); + } + return cached(uncached, 60000, object.id); +} + +function getGroupInfo(gr) { + if (defined(groupInfo[gr])) + return groupInfo[gr]; + groupInfo[gr] = new GroupInfo(); + return groupInfo[gr]; +} + +function groupTargetLabel(gr) { + return "NullBot_" + me + "_GroupTarget_" + gr; +} + +function groupMicroTargetLabel(gr) { + return "NullBot_" + me + "_GroupMicroTarget_" + gr; +} + +function vtolTargetLabel() { + return "NullBot_" + me + "_VtolTarget"; +} + +function findTarget(gr) { + var obj = safeGetObject(groupMicroTargetLabel(gr)); + var obj2 = safeGetObject(groupTargetLabel(gr)); + getGroupInfo(gr); + if (gameTime > groupInfo[gr].lastAttacked + 10000 && defined(obj2) && obj2.type === POSITION) + removeLabel(groupMicroTargetLabel(gr)); + if (!defined(obj)) + obj = obj2; + if (defined(obj)) { + if (gr === miscGroup) { + // harass group should switch targets when its target gets protected, otherwise targets are permanent + if (throttled(10000) || targetSuitableForHarass(obj)) + return obj; + } else + return obj; + } + // find harass targets for the misc group + if (gr === miscGroup) { + var list = enumStructList(miscTargets, enumLivingPlayers().filter(isEnemy).random()); + powerUps.forEach(function(stat) { // pick up oil drums and artifacts + list = list.concat(enumFeature(-1, stat)); + }); + list = list.filter(targetSuitableForHarass).filter(function(feature) { + if (iHaveHover()) + if (canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER)[0], feature)) + return true; + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND)[0], feature); + }).sort(function(one, two) { + return distanceToBase(one) - distanceToBase(two); + }); + obj = list[random(Math.min(3, list.length))]; + if (obj) { + addLabel(obj, groupTargetLabel(gr)); + return obj; + } + } + // fund structure targets + var list = enumStructList(targets, gr); + if (list.length > 0) + obj = list.random(); + else { + // find remaining droids + list = enumDroid(gr); + if (list.length > 0) + obj = list.random(); + } + if (defined(obj)) { + addLabel(obj, groupTargetLabel(gr)); + return obj; + } +} + +function groupInDanger(gr) { + getGroupInfo(gr); // make sure everything is defined + if (!defined(groupInfo[gr].lastAttacked)) + return false; + return gameTime - groupInfo[gr].lastAttacked < 10000; +} + +function regroup(gr) { + if (inPanic()) + return enumGroup(gr).filter(checkRepaired); + var size = attackGroupSize(); + if (size < groupSize(gr) / 2) + size = groupSize(gr) / 2; + var ret = naiveFindClusters(enumGroup(gr).filter(checkRepaired), (baseScale / 3)); + if (ret.maxCount === 0) + return []; + for (var i = 0; i < ret.clusters.length; ++i) + if (i !== ret.maxIdx) + for (var j = 0; j < ret.clusters[i].length; ++j) + orderDroidLoc(ret.clusters[i][j], DORDER_MOVE, ret.xav[ret.maxIdx], ret.yav[ret.maxIdx]); + if (ret.maxCount < size) { + for (var j = 0; j < ret.clusters[ret.maxIdx].length; ++j) { + if (groupInDanger(gr)) + orderDroid(ret.clusters[ret.maxIdx][j], DORDER_RTB); + else + orderDroid(ret.clusters[ret.maxIdx][j], DORDER_STOP); + } + return []; + } + return ret.clusters[ret.maxIdx]; +} + +function weHaveRepair() { + function uncached() { + return enumStruct(me, REPAIR_FACILITY).length > 0; + } + return cached(uncached, 1000); +} + +function checkRepaired(droid) { + if (!weHaveRepair()) + return true; + if (droid.order === DORDER_RTR) + return false; + if (droid.health < personality.repairAt) { + orderDroid(droid, DORDER_RTR); + return false; + } + return true; +} + +function droidFree(droid) { + if (droid.order === DORDER_SCOUT) + return false; + if (droid.order === DORDER_ATTACK) + return false; + if (droid.order === DORDER_RTR) + return false; + if (!checkRepaired(droid)) + return false; + return true; +} + +function attackTarget(droid) { + var target = findTarget(droid.group); + if (droid.group !== miscGroup) + if (!defined(target) || !droidCanReach(droid, target.x, target.y)) { + groupDroid(droid); + return; + } + if (defined(target)) + switch (target.type) { + case DROID: + if (droid.droidType !== DROID_SENSOR) + orderDroidObj(droid, DORDER_ATTACK, target); + else + orderDroidObj(droid, DORDER_OBSERVE, target); + break; + case FEATURE: + orderDroidObj(droid, DORDER_RECOVER, target); + break; + case STRUCTURE: + if (droid.droidType !== DROID_SENSOR) + orderDroidLoc(droid, DORDER_SCOUT, target.x, target.y); + else + orderDroidObj(droid, DORDER_OBSERVE, target); + break; + default: + orderDroidLoc(droid, DORDER_SCOUT, target.x, target.y); + break; + } +} + +function vtolCanHit(droid, obj) { + if (typeof(obj) === DROID && isVTOL(obj)) + return droid.canHitAir; + else + return droid.canHitGround; +} + +function pickVtolTarget(droid) { + function uncached() { + function canHit(obj) { + return vtolCanHit(droid, obj); + } + var enemy = enumLivingPlayers().filter(isEnemy).random(); + var list; + list = enumStructList(miscTargets, enemy).filter(canHit); + if (list.length > 0) return list.random(); + list = enumStruct(enemy, DEFENSE).filterProperty("canHitAir", true).filter(canHit); + if (list.length > 0) return list.random(); + list = enumDroid(enemy, DROID_WEAPON).filterProperty("canHitAir", true).filter(canHit); + if (list.length > 0) return list.random(); + list = enumDroid(enemy, DROID_CYBORG).filterProperty("canHitAir", true).filter(canHit); + if (list.length > 0) return list.random(); + list = enumStructList(targets, enemy).filter(canHit); + if (list.length > 0) return list.random(); + list = enumDroid(enemy).filter(canHit); + if (list.length > 0) return list.random(); + } + return cached(uncached, 100, droid.canHitAir + 2 * droid.canHitGround); +} + +function vtolArmed(obj, percent) { + if (obj.type != DROID) + return; + if (!isVTOL(obj)) + return false; + for (var i = 0; i < obj.weapons.length; ++i) + if (obj.weapons[i].armed >= 99) + return true; + return false; +} + +function vtolReady(droid) { + if (droid.order == DORDER_ATTACK) + return false; + if (vtolArmed(droid, 99)) + return true; + if (droid.order != DORDER_REARM) { + orderDroid(droid, DORDER_REARM); + buildVtols() // actually pads + } + return false; +} + +_global.attackGroupSize = function() { + var ret = personality.minTanks + (gameTime / 300000) * personality.becomeHarder; + if (ret > personality.maxTanks) + ret = personality.maxTanks; + return ret; +} + +_global.setTarget = function(object, group) { + if (!defined(group)) { + group = findNearestGroup(object.x, object.y); + if (object.type === STRUCTURE || object.type === DROID) + addLabel(object, vtolTargetLabel()); + } + if (!defined(group)) + return false; + if (defined(safeGetObject(groupTargetLabel(group)))) + if (throttled(10000, group)) // don't switch targets too often + return false; + if (object.type === DROID || (object.type === STRUCTURE && object.stattype === DEFENSE)) + addLabel(object, groupMicroTargetLabel(group)); + else + addLabel(object, groupTargetLabel(group)); + return true; +} + +_global.unsetTarget = function(player) { + for (var i = 0; i <= MAX_GROUPS; ++i) { + var obj = safeGetObject(groupTargetLabel(i)); + if (defined(obj) && obj.type === POSITION && findBeaconPlayer(obj.x, obj.y) === player) + removeLabel(groupTargetLabel(i)); + } +} + +_global.groupDroid = function(droid) { + if (droid.droidType === DROID_WEAPON || droid.droidType === DROID_CYBORG) { + if (isVTOL(droid)) { + groupAdd(vtolGroup, droid); + return; + } + if (withChance(100 - groupSize(miscGroup) * 50 / personality.maxMiscTanks) || firstTimeHarass) { + firstTimeHarass = false; + groupAdd(miscGroup, droid); + return; + } + var grp = groupsBySize().filter(function(i) { + if (isAlly(i)) + return false; + if (!defined(findTarget(i))) + return false; + if (!droidCanReach(droid, findTarget(i).x, findTarget(i).y)) + return false; + return true; + }); + var ret = grp.filter(function(i) { + return groupSize(i) < attackGroupSize() * 2 && defined(findTarget(i)); + }); + if (ret.length === 0) + ret = grp; + if (ret.length === 0) + ret = [ miscGroup ]; + groupAdd(ret[0], droid); + } + if (droid.droidType === DROID_SENSOR) + groupAdd(miscGroup, droid); +} + +_global.rebalanceGroups = function() { + if (throttled(5000)) + return; + if (groupSize(miscGroup) > personality.maxMiscTanks) { + var list = enumGroup(miscGroup).shuffle(); + for (var i = personality.maxMiscTanks; i < personality.maxMiscTanks + 5 && i < list.length; ++i) + groupDroid(list[i]); + } + var ret = groupsBySize(); + if (ret.length > 0) + if (ret[0] > 0 && ret[0] < attackGroupSize()) + for (var i = 1; i < ret.length; ++i) { + var list = enumGroup(ret[i]); + for (var j = 0; j < list.length; ++j) { + target = findTarget(ret[0]); + if (defined(target)) + if (droidCanReach(list[j], target.x, target.y)) { + groupAdd(ret[0], list[j]); + return; + } + } + } +} + +_global.touchGroup = function(gr) { + getGroupInfo(gr); // make sure everything is defined + groupInfo[gr].lastAttacked = gameTime; +} + +_global.fallBack = function(droid, threat) { + if (droid.order === DORDER_MOVE) + return; + if (guessDroidMicro(droid) === MICRO.MELEE) + return; + var x = droid.x - (baseScale / 7) * (threat.x - droid.x) / distance(threat, droid); + var y = droid.y - (baseScale / 7) * (threat.y - droid.y) / distance(threat, droid); + if (x < 1 || y < 1 || x > mapWidth - 2 || y > mapHeight - 2) + return; + orderDroidLoc(droid, DORDER_MOVE, x, y); +} + +_global.dangerLevel = function(loc) { + return enumRange(loc.x, loc.y, baseScale / 2, ENEMIES, false).length; +} + +_global.checkAttack = function() { + for (var i = 0; i < MAX_GROUPS; ++i) + if (!throttled(3000, i)) { + regroup(i).forEach(attackTarget); + break; + } + if (throttled(1000, "misc")) + return; + enumGroup(miscGroup).filter(checkRepaired).forEach(attackTarget); + if (throttled(5000, "vtols")) + return; + var droids = enumGroup(vtolGroup).filter(vtolReady); + if (droids.length > attackGroupSize() / 3.) + droids.forEach(function(droid) { + var target = pickVtolTarget(droid); + if (defined(target)) + orderDroidObj(droid, DORDER_ATTACK, target); + }); +} + +_global.pushVtols = function(object) { + var vtols = enumRange(object.x, object.y, 20, me, false); + var enemies = enumRange(object.x, object.y, 8, ENEMIES, true); + for (var i = 0; i < vtols.length; ++i) + if (vtolArmed(vtols[i], 1)) + for (var j = 0; j < enemies.length; ++j) + if (vtolCanHit(vtols[i], enemies[j])) { + orderDroidObj(vtols[i], DORDER_ATTACK, enemies[j]); + break; + } +} + +_global.inPanic = function() { + function uncached() { + var badGuys = enumRange(baseLocation.x, baseLocation.y, baseScale, ENEMIES).length; + var goodGuys = enumRange(baseLocation.x, baseLocation.y, baseScale, ALLIES).filter(function(object) { + return object.type === DROID && (object.droidType === DROID_WEAPON || object.droidType === DROID_CYBORG); + }).length; + return 3 * badGuys > 2 * goodGuys; + } + return cached(uncached, 10000); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes-custom/timers.js b/har_includes-custom/timers.js new file mode 100644 index 0000000..ec3f66e --- /dev/null +++ b/har_includes-custom/timers.js @@ -0,0 +1,26 @@ + +/* + * This file lists all timers used by the AI. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// slightly shift all timers so that not to get too many of them on the same game frame, +// especially when multiple AI instances are running +function rnd() { + return random(201) - 100; +} + +_global.setTimers = function() { + setTimer("spendMoney", 2000 + 3 * rnd()); + setTimer("checkAttack", 100); + setTimer("adaptCycle", 100); + setTimer("rebalanceGroups", 10000 + 20 * rnd()); + if (difficulty === EASY) + setTimer("goEasy", 30000); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/_events.js b/har_includes/_events.js new file mode 100644 index 0000000..156a655 --- /dev/null +++ b/har_includes/_events.js @@ -0,0 +1,105 @@ + +/* + * This file includes event definitions only. + * + */ + +function eventStartLevel() { + queue("setTimers", me * 100); + if (alliancesType === ALLIANCES_TEAMS) { + // initialize subpersonality pseudo-randomly here + // to make sure teammates have the same personality + var j = 1, s = 0; + for (var i = 0; i < maxPlayers; ++i) { + if (allianceExistsBetween(me, i)) + s += j; + j *= 2; + } + // the random "s" number obtained here is the same for all players in any team + var s = s + (new Date()).getMinutes(); + j = 0; + for (var i in subpersonalities) // count the amount of subpersonalities + ++j; + s = s % j; + j = 0; + for (var i in subpersonalities) { + if (j === s) + personality = subpersonalities[i]; + ++j; + } + } else { + // if teams are not sharing research, or there are no teams at all, + // initialize the subpersonality randomly and don't care + personality = randomItem(subpersonalities); + } + // the following code is necessary to avoid some strange game bug when droids that + // are initially buried into the ground fail to move out of the way when a building + // is being placed right above them + enumTrucks().forEach(function(droid) { + orderDroidLoc(droid, DORDER_MOVE, droid.x + random(3) - 1, droid.y + random(3) - 1); + }); +} + +function eventDroidBuilt(droid, structure) { + groupDroid(droid); +} + +function eventStructureBuilt(structure) { + queue("checkConstruction"); +} + +function eventAttacked(victim, attacker) { + if (attacker === null) + return; // no idea why it happens sometimes + if (isAlly(attacker.player)) + return; // don't respond to accidental friendly fire + if (victim.type === DROID) { + if (!isVTOL(victim) && defined(victim.group)) { + fallBack(victim, attacker); + setTarget(attacker, victim.group); + touchGroup(victim.group); + } else if (isVTOL(victim) && victim.player == me && !throttled(5000, victim.id)) { + orderDroidObj(victim, DORDER_ATTACK, attacker); + pushVtols(attacker); + } + } else if (victim.type === STRUCTURE) { + if (throttled(5000) && victim.player != me) + return; + if (inPanic()) + for (var i = 0; i < MAX_GROUPS; ++i) + if (groupSize(i) > 0) + setTarget(attacker, i); + setTarget(attacker, miscGroup); + setTarget(attacker); + } +} + +function eventStructureReady(structure) { + fireLassat(structure); +} + +function eventChat(from, to, message) { + // we are not case-sensitive + message = message.toLowerCase(); + handleChatMessage(from, to, message) +} + +function eventObjectTransfer(object, from) { + if (object.player !== me) + return; // object was transferred from me, not to me + if (object.type === DROID) + groupDroid(object); +} + +function eventBeacon(x, y, from, to) { + noticeBeacon(x, y, from); +} + +function eventBeaconRemoved(from, to) { + unnoticeBeacon(from); +} + +function eventDestroyed(object) { + if (isEnemy(object.player)) + pushVtols(object); +} diff --git a/har_includes/_head.js b/har_includes/_head.js new file mode 100644 index 0000000..75df8ef --- /dev/null +++ b/har_includes/_head.js @@ -0,0 +1,107 @@ + +/* + * This file defines generic things that should be defined + * prior to defining a ruleset. + * + * NOTE: This file is not included from main.js . + * + */ + +// weapon path role +const ROLE = { + AT: 0, + AP: 1, + AS: 2, + AA: 3, + LENGTH: 4, // number of items in this enum +} + +// something dual to the previous enum +const OBJTYPE = { + TANK: 0, + BORG: 1, + DEFS: 2, + VTOL: 3, + LENGTH: 4, // number of items in this enum; should be equal to ROLE.LENGTH +} + +// this controls body and weapon compatibility. +// A little explanation: +// w \ b | L | M | H bodies can't be "ultra-", +// UL | + | - | - ultra-light weapons are for light bodies only, +// L | + | + | - light weapons are for light or medium bodies, +// M | + | + | + medium weapons are for all bodies, +// H | - | + | + heavy weapons are for medium or heavy bodies, +// UH | - | - | + ultra-heavy weapons are for heavy bodies only. +const WEIGHT = { + ULTRALIGHT: 0, + LIGHT: 1, + MEDIUM: 2, + HEAVY: 3, + ULTRAHEAVY: 4, +} + +// what to use this defensive structure for +const DEFROLE = { + STANDALONE: 0, + GATEWAY: 1, + ARTY: 2, + ANTIAIR: 3, + FORTRESS: 4, +} + +// return values of build order calls +const BUILDRET = { + SUCCESS: 0, + UNAVAILABLE: 1, + FAILURE: 2, +} + +// should we execute a build call in an unsafe location? +const IMPORTANCE = { + PEACETIME: 0, + MANDATORY: 1, +} + +// aspects of every research path +const RESASPECTS = { + WEAPONS: 0, + DEFENSES: 1, + VTOLS: 2, + EXTRAS: 3, + LENGTH: 4, // number of items in this enum +} + +const PROPULSIONUSAGE = { + GROUND : 1, + HOVER: 2, + VTOL: 4 +} + +// what to use this body for? (bit field) +const BODYUSAGE = { + GROUND: 1, // for tanks + AIR: 2, // for VTOLs + COMBAT: 3, // GROUND | AIR + TRUCK: 4, // trucks, sensors and repair droids + UNIVERSAL: 7, // COMBAT | TRUCK +} + +// what sort of weapons this body is resistant to? +const BODYCLASS = { + KINETIC: 1, + THERMAL: 2, +} + +// should this module be prioritized when not having enough power? +const MODULECOST = { + CHEAP: 0, + EXPENSIVE: 1, +} + +// how to micromanage tanks with this sort of weapon +const MICRO = { + RANGED: 0, // normal weapons, such as cannons + MELEE: 1, // weapons that need to get very close to the enemy, such as flamers + DUMB: 2, // weapons that can't fire while moving +} diff --git a/har_includes/_main.js b/har_includes/_main.js new file mode 100644 index 0000000..3e15d9a --- /dev/null +++ b/har_includes/_main.js @@ -0,0 +1,26 @@ + +/* + * This file connects all remaining pieces of AI code together. + * It shouldn't contain any code itself. + * + * NOTE: order matters! + * + */ + +var personality; + +include(HAR_INCLUDES + "math.js"); +include(HAR_INCLUDES + "intensity.js"); +include(HAR_INCLUDES + "misc.js"); +include(HAR_INCLUDES + "stats.js"); + +include(HAR_INCLUDES + "adapt.js"); +include(HAR_INCLUDES + "build.js"); +include(HAR_INCLUDES + "research.js"); +include(HAR_INCLUDES + "produce.js"); +include(HAR_INCLUDES + "tactics.js"); +include(HAR_INCLUDES + "lassat.js"); +include(HAR_INCLUDES + "chat.js"); + +include(HAR_INCLUDES + "timers.js"); +include(HAR_INCLUDES + "_events.js"); diff --git a/har_includes/adapt.js b/har_includes/adapt.js new file mode 100644 index 0000000..3c05501 --- /dev/null +++ b/har_includes/adapt.js @@ -0,0 +1,484 @@ + +/* + * This file is responsible for the adaptation system. It gathers statistics about + * player choices and regulates the AI's choices accordingly. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +function adaptVote(our, their, verbose) { + if (!defined(verbose)) + verbose = false; + var l = our.length; + var ourTotal = our.reduce(function(prev, curr) { return prev + curr; }); + var theirTotal = their.reduce(function(prev, curr) { return prev + curr; }); + if (theirTotal === 0) + return verbose ? randomUnitArray(l) : random(l); + var rating = []; + for (var i = 0; i < l; ++i) + rating[i] = their[i] / theirTotal; + if (ourTotal > 0) + for (var i = 0; i < l; ++i) + rating[i] -= our[i] / ourTotal; + if (verbose) + return rating.map(function(val) { return (val + 1) / 2; }); + var maxRating = -Infinity; + var maxIdx = 0; + for (var i = 0; i < l; ++i) + if (rating[i] > maxRating) { + maxRating = rating[i]; + maxIdx = i; + } + return maxIdx; +} + +// +// here be functions for gathering player statistics +// + +function SimpleStat() { + // propulsion classes used by the player + this.obj = zeroArray(ROLE.LENGTH); + // weapon-propulsion classes used by the player + this.role = zeroArray(ROLE.LENGTH); + // armor used by the player + this.kbody = 0; + this.tbody = 0; + // weapon-armor classes used by the player + this.kweapon = 0; + this.tweapon = 0; +} + +function addStat(to, what, weight) { + if (!defined(weight)) + weight = 1; + for (var prop in to) { + if (to[prop].constructor === Array) + for (var i = 0; i < to[prop].length; ++i) + to[prop][i] += what[prop][i] * weight; + else + to[prop] += what[prop] * weight; + } +} + +const SCOPES = { + land: 0, + sea: 0, + air: 0, +} + +function ScopeStat() { + // simple stats by scopes + this.land = new SimpleStat(); + this.sea = new SimpleStat(); + this.air = new SimpleStat(); + // a function to convert this sort of stat to a SimpleStat + this.collapse = function() { + var ret = new SimpleStat(); + for (var i in SCOPES) + addStat(ret, this[i]); + return ret; + } +} + +function EnemyStat() { + // separate stats for defense and offense + this.defense = new ScopeStat(); + this.offense = new SimpleStat(); + // a function to convert this sort of stat to a SimpleStat + this.collapse = function() { + var ret = new SimpleStat(); + addStat(ret, this.defense.collapse()); + addStat(ret, this.offense); + return ret; + } +} + +function MyStat() { + // separate stats for defense and offense + this.defense = new SimpleStat(); + this.offense = new ScopeStat(); + // a function to convert this sort of stat to a SimpleStat + this.collapse = function() { + var ret = new SimpleStat(); + addStat(ret, this.defense); + addStat(ret, this.offense.collapse()); + return ret; + } +} + +var enemyStats = [], enemyStatsTemp = []; +for (var i = 0; i < maxPlayers; ++i) { + enemyStats[i] = new EnemyStat; + enemyStatsTemp[i] = new EnemyStat; +} + +var myStats = new MyStat; +var myStatsTemp = new MyStat; + +var stack = []; +var MAX_PER_CYCLE = 20; + +function canReachBy(scope, location) { + switch (scope) { + case "land": + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND).last(), location); + case "sea": + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER).last(), location); + case "air": + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.AIR).last(), location); + } +} + +// works with stored droid objects too! +function threatensBase(droid) { + if (isAlly(droid.player)) + return false; + return canReachFromBase(droid.propulsion, droid.x, droid.y); +} + +// count target structures reachable by land +function countLandTargets(player) { + function uncached() { + var currProp = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND).last(); + if (!defined(currProp)) + return 0; + var list = enumStructList(targets, player); + return list.filter(function(struct) { + return canReachFromBase(currProp, struct); + }).length; + } + return cached(uncached, 5000, player); +} + +// count target structures reachable by sea but not by land +function countSeaTargets(player) { + function uncached() { + var currProp = getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER)[0]; + if (!defined(currProp)) + return 0; + var prevProp = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND)[0]; + return enumStructList(targets, player).filter(function(struct) { + return (!defined(prevProp) || !canReachFromBase(prevProp, struct)) && canReachFromBase(currProp, struct); + }).length; + } + return cached(uncached, 5000, player); +} + +// count target structures reachable by air but not by land or by sea +function countAirTargets(player) { + function uncached() { + var currProp = getPropulsionStatsComponents(PROPULSIONUSAGE.VTOL)[0]; + if (!defined(currProp)) + return 0; + var prevProp = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND|PROPULSIONUSAGE.HOVER)[0]; + return enumStructList(targets, player).filter(function(struct) { + return (!defined(prevProp) || !canReachFromBase(prevProp, struct)) && canReachFromBase(currProp, struct); + }).length; + } + return cached(uncached, 5000, player); +} + +// TODO: this function needs to be updated once more object properties +// become available for the API to use. +function classifyObject(obj) { + var ret = new SimpleStat(); + if (obj.type === STRUCTURE && obj.stattype !== DEFENSE) + return ret; + if (obj.type === DROID && obj.droidType !== DROID_WEAPON && obj.droidType !== DROID_CYBORG) + return ret; + if (obj.type === FEATURE) + return ret; + for (var i = 0; i < obj.weapons.length; ++i) { + var roles = guessWeaponRole(obj.weapons[i].name); { + if (!defined(roles)) { + if (obj.canHitAir && obj.canHitGround) + ret.role.addArray([1/4, 1/4, 1/4, 1/4]); + else if (obj.canHitAir && !obj.canHitGround) + ret.role.addArray([0, 0, 0, 1]); + else + ret.role.addArray([1/3, 1/3, 1/3, 0]); + } else + ret.role.addArray(roles); + } + } + for (var i = 0; i < obj.weapons.length; ++i) { + if (getWeaponInfo(obj.weapons[i].name).impactClass === "KINETIC") + ret.kweapon += 1; + else + ret.tweapon += 1; + } + if (obj.type === STRUCTURE || (obj.type === DROID && safeIsVtol(obj))) + ret.tbody += 1; + else if (obj.type === DROID && obj.droidType === DROID_CYBORG) + ret.kbody += 1; + else { + switch (guessBodyArmor(obj.body)) { + case BODYCLASS.KINETIC: + ret.kbody += 1; + break; + case BODYCLASS.THERMAL: + ret.tbody += 1; + break; + default: + ret.tbody += 1 / 2; + ret.kbody += 1 / 2; + } + } + if (obj.type === STRUCTURE) + ret.obj[OBJTYPE.DEFS] += 1; + if (obj.type === DROID) { + if (safeIsVtol(obj)) + ret.obj[OBJTYPE.VTOL] += 1; + else if (obj.droidType === DROID_CYBORG) + ret.obj[OBJTYPE.BORG] += 1; + else + ret.obj[OBJTYPE.TANK] += 1; + } + return ret; +} + +function summUpEnemyObject(obj, stat) { + var ret = classifyObject(obj); + var w = obj.cost; + if (obj.type === STRUCTURE) { + for (var scope in SCOPES) + if (canReachBy(scope, obj)) // structures don't move, so they are usually counted as defense + addStat(stat.defense[scope], ret, w); + if (obj.range > baseScale * 2) // unless they're covering the whole map with their range + addStat(stat.offense, ret, w); + } + if (obj.type === DROID) { + for (var scope in SCOPES) + if (canReachBy(scope, obj)) // if the droid can't reach your base, we count it as defense only + addStat(stat.defense[scope], ret, w); + if (threatensBase(obj)) // otherwise count them as offense as well + addStat(stat.offense, ret, w); + } +} + +function summUpMyObject(obj, stat) { + var ret = classifyObject(obj); + var w = obj.cost; + if (obj.type === STRUCTURE) { + addStat(stat.defense, ret, w); + if (obj.range > baseScale * 2) + for (var scope in SCOPES) + addStat(stat.offense[scope], ret, w); + } + if (obj.type === DROID) { + if (obj.group === miscGroup) + addStat(stat.defense, ret, w); + var list = enumLivingPlayers(); + list.forEach(function(p) { + if (isEnemy(p)) { + if (countLandTargets(p) > 0) + addStat(stat.offense.land, ret, w / list.length); + if (countSeaTargets(p) > 0) + addStat(stat.offense.sea, ret, w / list.length); + if (countAirTargets(p) > 0) + addStat(stat.offense.air, ret, w / list.length); + } + }); + } +} + +_global.adaptCycle = function() { + if (!defined(adaptCycle.player)) + adaptCycle.player = me; + if (!defined(adaptCycle.phase)) + adaptCycle.phase = 0; + if (stack.length > 0) { + var items = MAX_PER_CYCLE; + if (items > stack.length) + items = stack.length; + for (var i = 0; i < items; ++i) { + var obj = stack.pop(); + if (isEnemy(adaptCycle.player)) + summUpEnemyObject(obj, enemyStatsTemp[adaptCycle.player]); + else if (adaptCycle.player === me) + summUpMyObject(obj, myStatsTemp); + } + return; + } + ++adaptCycle.phase; + switch(adaptCycle.phase) { + case 1: + stack = enumStruct(adaptCycle.player, DEFENSE); + return; + case 2: + stack = enumDroid(adaptCycle.player, DROID_WEAPON); + return; + case 3: + stack = enumDroid(adaptCycle.player, DROID_CYBORG); + return; + } + if (adaptCycle.player === me) { + myStats = myStatsTemp; + myStatsTemp = new MyStat; + } else { + enemyStats[adaptCycle.player] = enemyStatsTemp[adaptCycle.player]; + enemyStatsTemp[adaptCycle.player] = new EnemyStat; + } + do { + ++adaptCycle.player; + if (adaptCycle.player >= maxPlayers) + adaptCycle.player = 0; + } while (adaptCycle.player !== me && !isEnemy(adaptCycle.player)); + adaptCycle.phase = 0; +} + +function getMyGroupInfo(gr) { + var ret = new MyStat(); + enumGroup(gr).forEach(function(obj) { summUpMyObject(obj, ret); }); + return ret; +} + +// +// here be functions for querying statistics gathered above +// and making adaptive decisions +// + +function groupOurs(gr) { + function uncached() { + if (defined(gr)) + return getMyGroupInfo(gr).collapse(); + else + return myStats.collapse(); + } + return cached(uncached, 500, gr); +} + +function groupAttackOurs(gr) { + function uncached() { + if (defined(gr)) + return getMyGroupInfo(gr).collapse(); + else + return myStats.offense.collapse(); + } + return cached(uncached, 500, gr); +} + +function enemyOffense() { + var theirs = new SimpleStat(); + enumLivingPlayers().filter(isEnemy).forEach(function(p) { + addStat(theirs, enemyStats[p].offense); + }); + return theirs; +} + +function groupTheirs(gr) { + function uncached() { + if (defined(gr)) { + if (gr === miscGroup) + return enemyOffense(); + else + return enemyStats[gr].collapse(); + } else { + var theirs = new SimpleStat(); + enumLivingPlayers().filter(isEnemy).forEach(function(p) { + addStat(theirs, enemyStats[p].collapse()); + }); + return theirs; + } + } + return cached(uncached, 500, gr); +} + +// use this for research; 15% smooth +_global.chooseWeaponRole = function(gr) { + function uncached() { + if (withChance(20)) + return randomUnitArray(4); + return adaptVote(groupOurs(gr).role, groupTheirs(gr).obj, true); + } + return cached(uncached, 5000, gr); +} + +// use this for droids and long-range arty emplacements +_global.chooseAttackWeaponRole = function(gr) { + function uncached() { + return adaptVote(groupAttackOurs(gr).role, groupTheirs(gr).obj, true); + } + return cached(uncached, 5000, gr); +} + +// use this for defenses; 15% smooth +_global.chooseDefendWeaponRole = function() { + function uncached() { + if (withChance(20)) + return randomUnitArray(4); + return adaptVote(myStats.defense.role, enemyOffense().obj, true); + } + return cached(uncached, 5000); +} + +_global.chooseBodyClass = function(gr) { + function uncached() { + var our = groupAttackOurs(gr), their = groupTheirs(gr); + return adaptVote( + [ our.kbody, our.tbody ], + [ their.kweapon, their.tweapon ] + ) ? BODYCLASS.THERMAL : BODYCLASS.KINETIC; + } + return cached(uncached, 5000, gr); +} + +_global.chooseObjectType = function() { + function uncached() { + var our = groupOurs(), their = groupTheirs(); + // behaviour specific for a turtle AI + if (personality.defensiveness === 100) + if (iHaveVtol() && withChance(personality.vtolness) && adaptVote( + [ our.obj[OBJTYPE.DEFS], our.obj[OBJTYPE.VTOL] ], + [ their.role[ROLE.AA], their.role[ROLE.AT] + their.role[ROLE.AP] + 2 * their.role[ROLE.AS] ] + ) === 1) + return OBJTYPE.VTOL; + else + return OBJTYPE.DEFS; + // behaviour of a generic AI + if (withChance(personality.defensiveness) && adaptVote( + [ our.obj[OBJTYPE.TANK] + our.obj[OBJTYPE.BORG] + our.obj[OBJTYPE.VTOL], our.obj[OBJTYPE.DEFS] ], + [ their.role[ROLE.AS], their.role[ROLE.AT] + their.role[ROLE.AP] + their.role[ROLE.AA] ] + ) === 1) + return OBJTYPE.DEFS; + if (iHaveVtol() && withChance(personality.vtolness) && adaptVote( + [ our.obj[OBJTYPE.TANK] + our.obj[OBJTYPE.BORG] + our.obj[OBJTYPE.DEFS], our.obj[OBJTYPE.VTOL] ], + [ their.role[ROLE.AA], their.role[ROLE.AT] + their.role[ROLE.AP] + their.role[ROLE.AS] ] + ) === 1) + return OBJTYPE.VTOL; + return adaptVote( + [ our.obj[OBJTYPE.TANK], our.obj[OBJTYPE.BORG] ], + [ their.role[ROLE.AP], their.role[ROLE.AT] ] + ) ? OBJTYPE.BORG : OBJTYPE.TANK; + } + return cached(uncached, 5000); +} + +_global.scopeRatings = function() { + function uncached() { + var ret = { land: 0, sea: 0, air: 0 }; + enumLivingPlayers().filter(isEnemy).forEach(function(player) { + ret.land += countLandTargets(player); + ret.sea += countSeaTargets(player); + ret.air += countAirTargets(player); + }); + return ret; + } + return cached(uncached, 5000); +} + +_global.spendMoney = function() { + queue("checkResearch", 100); + if (checkTruckProduction()) + return; // will proceed on the next cycle + if (chooseObjectType() == OBJTYPE.DEFS) + queue("checkConstruction", 200); + else + queue("checkProduction", 300); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/build.js b/har_includes/build.js new file mode 100644 index 0000000..e506eab --- /dev/null +++ b/har_includes/build.js @@ -0,0 +1,368 @@ + +/* + * This file describes building construction procedures. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +function randomLocation() { + var x = baseLocation.x + random(baseScale) - baseScale / 2; + var y = baseLocation.y + random(baseScale) - baseScale / 2; + if (x < 3 || y < 3 || x > mapWidth - 4 || y > mapHeight - 4) + return baseLocation; + return {x: x, y: y}; + +} + +// a function for checking the presence of enemy units at the construction site +function safeSpot(x, y) { + return dangerLevel({x: x, y: y}) <= 0; +} + +function truckFree(truck) { + if (truck.droidType !== DROID_CONSTRUCT) + return false; + if (truck.order === DORDER_BUILD) + return false; + if (truck.order === DORDER_HELPBUILD) + return false; + if (truck.order === DORDER_LINEBUILD) + return false; + if (truck.order === DORDER_DEMOLISH) + return false; + return true; +} + +// returns one or two free trucks +function getTwoFreeTrucks() { + var trucks = enumTrucks().filter(truckFree); + if (trucks.length > 2) { + var ret = naiveFindClusters(trucks, baseScale / 2); + if (ret.maxCount >= 2) + trucks = ret.clusters[ret.maxIdx]; + } + if (trucks.length > 2) + trucks.length = 2; + return trucks; +} + +function getFreeTruckAround(x, y) { + var list = enumTrucks().filter(truckFree).filter(function(droid) { + return droidCanReach(droid, x, y); + }).sort(function(one, two) { + return distance(one, x, y) - distance(two, x, y); + }); + if (list.length > 0) + return list[0]; +} + +function buildModule(struct) { + trucks = getTwoFreeTrucks(); + if (trucks.length <= 0) + return BUILDRET.FAILURE; + var moduleInfo = modules.filter(function(item) { return isAvailable(item.module) && item.base === struct.stattype; }).last(); + if (!defined(moduleInfo)) + return BUILDRET.UNAVAILABLE; + if (struct.modules >= moduleInfo.count) + return BUILDRET.UNAVAILABLE; + var success = false; + for (var i = 0; i < trucks.length; ++i) + success = orderDroidBuild(trucks[i], DORDER_BUILD, moduleInfo.module, struct.x, struct.y) || success; + if (success) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function buildBasicStructure(statlist, importance) { + if (throttled(5000, statlist[0])) return BUILDRET.FAILURE; + // by default, don't try building things in dangerous locations + if (!defined(importance)) + importance = IMPORTANCE.PEACETIME; + trucks = getTwoFreeTrucks(); + if (trucks.length <= 0) + return BUILDRET.FAILURE; + // choose structure type (out of the statlist), + // together with suitable location + var idx, loc, avail = false; + for (var i = 0; i < statlist.length; ++i) + if (isAvailable(statlist[i])) { + avail = true; + if (distanceToBase(trucks[0]) <= baseScale) + loc = pickStructLocation(trucks[0], statlist[i], trucks[0].x, trucks[0].y); + else { + var rndLoc = randomLocation(); + loc = pickStructLocation(trucks[0], statlist[i],rndLoc.x, rndLoc.y); + } + idx = i; + break; + } + if (!avail) + return BUILDRET.UNAVAILABLE; + if (!defined(loc)) + return BUILDRET.FAILURE; + if (importance === IMPORTANCE.PEACETIME && !safeSpot(loc.x, loc.y)) + return BUILDRET.FAILURE; + // now actually build + var success = false; + for (var i = 0; i < trucks.length; ++i) + success = orderDroidBuild(trucks[i], DORDER_BUILD, statlist[idx], loc.x, loc.y) || success; + if (success) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function finishStructures() { + var success = false; + var list = enumStruct(me).filterProperty("status", BEING_BUILT); + for (var i = 0; i < list.length; ++i) { + if (success) + return; + if (throttled(10000, list[i].id)) + return; + if (list[i].stattype === RESOURCE_EXTRACTOR) + return; + var truck = getFreeTruckAround(list[i].x, list[i].y); + if (!defined(truck)) + return; + if (orderDroidObj(truck, DORDER_HELPBUILD, list[i])) + success = true; + }; + return success; +} + +function buildStructureAround(statlist, loc, unique) { + if (!defined(statlist)) + return BUILDRET.UNAVAILABLE; + var truck = getFreeTruckAround(loc.x, loc.y); + if (!defined(truck)) + return BUILDRET.FAILURE; + var stat = statlist.filter(isAvailable).filter(function(s) { + if (unique !== true) + return true; + var list = enumStruct(me, s); + for (var i = 0; i < list.length; ++i) + if (distance(list[i], loc) < baseScale / 2) + return false; + return true; + }).last(); + if (!defined(stat)) + return BUILDRET.UNAVAILABLE; + var loc2 = pickStructLocation(truck, stat, loc.x, loc.y); + if (!defined(loc2)) + return BUILDRET.FAILURE; + // if we're not into turtling, don't build too many towers + if (personality.defensiveness < 100 && distance(loc2, loc) > baseScale / 5) + return BUILDRET.FAILURE; + if (orderDroidBuild(truck, DORDER_BUILD, stat, loc2.x, loc2.y)) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function captureOil(oil) { + if (!defined(oil)) + return BUILDRET.FAILURE; + var truck = getFreeTruckAround(oil.x, oil.y); + if (!defined(truck)) + return BUILDRET.FAILURE; + var stat = structures.derricks.filter(isAvailable).last(); + if (!defined(stat)) + return BUILDRET.UNAVAILABLE; + if (throttled(90000, oil.y * mapWidth + oil.x)) + return BUILDRET.FAILURE; + if (orderDroidBuild(truck, DORDER_BUILD, stat, oil.x, oil.y)) + return BUILDRET.SUCCESS; + return BUILDRET.FAILURE; +} + +function chooseDefense(defrole) { + return weaponStatsToDefenses(chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseDefendWeaponRole(), 2, defrole), defrole); +} + +function buildTowers() { + var oils = enumStructList(structures.derricks); + if (oils.length === 0) + return false; + if (withChance(70)) + return buildStructureAround(chooseDefense(DEFROLE.STANDALONE), oils.random()) !== BUILDRET.UNAVAILABLE; + return buildStructureAround(chooseDefense(DEFROLE.FORTRESS).concat(structures.sensors), oils.random(), true) !== BUILDRET.UNAVAILABLE; +} + +function buildGateways() { + var oils = countStructList(structures.derricks); + if (oils <= 0) + return BUILDRET.FAILURE; + var gates = enumGateways().filter(function(gate) { + var l = gate.x1 - gate.x2 + gate.y1 - gate.y2; + if (l < 0) + l = -l; + var cnt = enumRange(gate.x1, gate.y1, l, ALLIES).filterProperty("stattype", DEFENSE).length; + cnt += enumRange(gate.x2, gate.y2, l, ALLIES).filterProperty("stattype", DEFENSE).length; + cnt -= enumRange(gate.x1, gate.y1, l, ENEMIES).filterProperty("stattype", DEFENSE).length; + cnt -= enumRange(gate.x2, gate.y2, l, ENEMIES).filterProperty("stattype", DEFENSE).length; + return cnt >= 0 && (cnt < l || (personality.defensiveness === 100 && withChance(70))); // turtle AI needs to keep building towers + }).sort(function(one, two) { return distanceToBase({x: one.x1, y: one.y1}) - distanceToBase({x: two.x1, y: two.y1}); }); + if (gates.length === 0) + return; + if (withChance(50)) + return buildStructureAround(chooseDefense(DEFROLE.GATEWAY), {x: gates[0].x1, y: gates[0].y1}) !== BUILDRET.UNAVAILABLE; + else + return buildStructureAround(chooseDefense(DEFROLE.GATEWAY), {x: gates[0].x2, y: gates[0].y2}) !== BUILDRET.UNAVAILABLE; +} + +function buildArty() { + return buildBasicStructure(chooseDefense(DEFROLE.ARTY)); +} + +_global.buildMinimum = function(statlist, count, importance) { + if (countStructList(statlist) < count) + if (buildBasicStructure(statlist, importance) !== BUILDRET.UNAVAILABLE) + return true; + return false; +} + +function captureSomeOil() { + if (throttled(500)) + return true; + function getOilList() { + var oils = []; + oilResources.forEach(function(stat) { oils = oils.concat(enumFeature(-1, stat)); }); + oils = oils.concat(enumStructList(structures.derricks).filterProperty("status", BEING_BUILT)); + oils = oils.sort(function(one, two) { + return distanceToBase(one) - distanceToBase(two); + }); + if (oils.length > 10) + oils.length = 10; + return oils; + } + var oils = cached(getOilList, 5000); + if (countFinishedStructList(structures.derricks) >= 4 * structListLimit(structures.gens)) + return false; + + for (var i = 0; i < oils.length; ++i) + if (captureOil(oils[i]) === BUILDRET.SUCCESS) + return true; + return false; +} + +_global.buildMinimumDerricks = function(count) { + if (countFinishedStructList(structures.derricks) < count) + if (captureSomeOil()) + return true; + return false; +} + +function buildExpand() { + if (myPower() > personality.maxPower) { + switch (chooseObjectType()) { + case 0: + if (buildMinimum(structures.factories, Infinity)) return true; + case 1: + if (buildMinimum(structures.templateFactories, Infinity)) return true; + case 3: + if (buildMinimum(structures.vtolFactories, Infinity)) return true; + } + } + return false; +} + +function buildEnergy() { + var oils = countFinishedStructList(structures.derricks); + var gens = countStructList(structures.gens); + if (oils > 4 * gens) + if (buildBasicStructure(structures.gens) !== BUILDRET.UNAVAILABLE) + return true; + if (withChance(50)) + if (captureSomeOil()) + return true; + return false; +} + +function buildModules() { + var str = []; + for (var i = 0; i < modules.length; ++i) { + str = enumStruct(me, modules[i].base); + for (var j = 0; j < str.length; ++j) + if (buildModule(str[j]) !== BUILDRET.UNAVAILABLE) + return true; + } + return false; +} + +_global.buildVtols = function() { + if (buildMinimum(structures.vtolPads, enumDroid(me, DROID_WEAPON).filter(isVTOL).length / 2)) + return true; + return false; +} + +function buildExtras() { + if (throttled(180000)) + return false; + if (buildBasicStructure(structures.extras) !== BUILDRET.UNAVAILABLE) + return true; + return false; +} + +function buildDefenses() { + if (chooseObjectType() !== 2) + return false; + if (withChance(33)) { + if (buildTowers()) // includes sensor towers and forts + return true; + } else if (withChance(50)) { + if (buildGateways()) + return true; + } else + if (buildArty()) + return true; + return false; +} + +function listOutdatedDefenses() { + for (var path in weaponStats) { + for (var role in DEFROLE) { + var list = weaponStatsToDefenses(weaponStats[path], DEFROLE[role]); + for (var i = 0; i < list.length - 2; ++i) + if (isAvailable(list[i + 2])) { + if (countStruct(list[i]) > 0) + return enumStruct(me, list[i]); + } + } + }; + return []; +} + +function recycleDefenses() { + var trucks = enumTrucks().filter(truckFree); + if (trucks.length <= 0) + return false; + var list = listOutdatedDefenses(); + for (var i = 0; i < list.length; ++i) + for (var j = 0; j < trucks.length; ++j) + if (droidCanReach(trucks[j], list[i].x, list[i].y)) { + orderDroidObj(trucks[j], DORDER_DEMOLISH, list[i]); + return true; + } + return false; +} + +_global.checkConstruction = function() { + if (enumTrucks().filter(truckFree).length === 0) + return; + if (functionSeries("construction", [ + finishStructures, + buildOrder, + buildExpand, + buildEnergy, + buildModules, + buildVtols, + buildExtras, + recycleDefenses, + buildDefenses, + ])) + queue("checkConstruction"); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/chat.js b/har_includes/chat.js new file mode 100644 index 0000000..2fb290a --- /dev/null +++ b/har_includes/chat.js @@ -0,0 +1,151 @@ +/* + * This file is responsible for chat listening. It contains handler functions for different chat + * messages; each such function takes message and player as a parameter and returns a reply string. + * All chat talking (such as calling for help) is coded in other places, not here. + * + * Messages are marked as translatable when necessary. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +var prefix = '!har'; + +// key: name in chat, value: function that will be executed +// function gets two params: sender and argument +var commandMap = { + set: chatSet, + res: chatRes, + truck: chatTruck, + power: chatMoney, + money: chatMoney, // alias for "power" + help: chatHelp, + go: chatHelp, // alias for "help" + tx: chatUnhelp, +} + +var beaconInfo = []; + +_global.noticeBeacon = function(x, y, from) { + beaconInfo[from] = { + x: x, + y: y, + exists: 1, + }; +} + +_global.unnoticeBeacon = function(from) { + beaconInfo[from].exists = 0; +} + +_global.findBeaconPlayer = function(x, y) { + for (var i = 0; i < beaconInfo.length; ++i) + if (defined(beaconInfo[i]) && beaconInfo[i].x === x && beaconInfo[i].y === y) + return i; +} + +_global.handleChatMessage = function(sender, receiver, message) { + // don't reply on any other message coming sender enemies + if (message === "!har who") { + chat(sender, chatWho(sender)); + return; + } + if (!isAlly(sender)) + return; + if (message === "help me!!") { // Try to understand Nexus AI's way of calling for help + chatHelp(sender); + return; + } + var result = message.split(/ +/); + if (result[0] !== prefix) + return; + var command = result[1]; + var argument = result[2]; + if (defined(commandMap[command])) + chat(sender, commandMap[command](sender, argument)); +} + +function chatWho(sender, argument) { + var str = "NullBot3 (" + scriptName + ") "; + switch(difficulty) { + case EASY: str += _("EASY"); break; + case MEDIUM: str=str + _("MEDIUM"); break; + case HARD: str=str + _("HARD"); break; + case INSANE: str=str + _("INSANE"); break; + } + if (isAlly(sender)) + str += (" ~" + personality.chatalias + "~"); + return str; +} + +function chatSet(sender, argument) { + var str = ""; + for (var i in subpersonalities) { + if (subpersonalities[i].chatalias === argument) { + personality = subpersonalities[i]; + return _("Personality change successful."); + } + str = str + " " + subpersonalities[i].chatalias; + } + return _("No such personality! Try one of these:") + str; +} + +function chatRes(sender, argument) { + if (argument === "cl") { + setForcedResearch(); // clear + return _("Forced research cleared, will research anything I want now."); + } + if (argument === "no") { + setForcedResearch(null); // clear + return _("Research blocked, will research nothing now."); + } + if (argument === "fn") { + setForcedResearch(fundamentalResearch); + return _("Researching fundamental technology."); + } + var str = " cl no fn"; + for (var i in weaponStats) { + if (weaponStats[i].chatalias === argument) { + setForcedResearch(weaponStatsToResList(weaponStats[i])); + return _("Researching ") + weaponStats[i].chatalias; + } + if (weaponStats[i].chatalias.indexOf("useless") < 0) + str = str + " " + weaponStats[i].chatalias; + } + return _("No such research path! Try one of these:") + str; +} + +function chatTruck(sender, argument) { + var droid = enumTrucks().random(); + if (!defined(droid)) + return _("Sorry, I have no trucks."); + if (donateObject(droid, sender)) { + addBeacon(droid.x, droid.y, sender); + return _("You can use this one."); + } + return _("Sorry, droid transfer failed."); +} + +function chatMoney(sender, argument) { + var power = Math.round(myPower()/3); + donatePower(power, sender); + return _("Power transferred."); +} + +function chatHelp(sender, argument) { + if (!defined(beaconInfo[sender]) || !beaconInfo[sender].exists) + return _("Please put a beacon!"); + if (setTarget({ x: beaconInfo[sender].x, y: beaconInfo[sender].y, type: POSITION })) + return _("Coming!"); + else + return _("Sorry, I don't have any free forces to send for help!"); +} + +function chatUnhelp(sender, argument) { + unsetTarget(sender); + return _("Any time, big boss!"); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/intensity.js b/har_includes/intensity.js new file mode 100644 index 0000000..41241b7 --- /dev/null +++ b/har_includes/intensity.js @@ -0,0 +1,95 @@ + +/* + * This file includes generic functions for improving CPU intensity, + * like caching and throttling mechanisms. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// to make sure some function is not called more often than every 2 seconds, +// put something like that at start the of its body: +// if (throttled(2000)) return; +// if your function requires several throttling cases, you can use the optional +// notes value: +// if (throttled(60000, player)) return; +// NOTE: it won't work if the function repeatedly dies and gets created again, eg. +// a function defined inside forEach(function(...){...}) can't be throttled +_global.throttled = function(interval, notes) { + if (!defined(arguments.callee.caller.throttleTimes)) + arguments.callee.caller.throttleTimes = {}; + if (!defined(arguments.callee.caller.throttleTimes[notes])) { + arguments.callee.caller.throttleTimes[notes] = gameTime; + return false; + } + if (gameTime - arguments.callee.caller.throttleTimes[notes] < interval) + return true; + arguments.callee.caller.throttleTimes[notes] = gameTime; + return false; +} + +// to cache a function's output value and make sure it's not re-calculated too often, +// use the following trick: +// function calculateValue(params) { +// function uncached(params) { +// // do heavy calculations +// } +// return cached(uncached, 10000); +// } +// add necessary notes to the cached() call if necessary, similar to how you do it +// for throttled(). +// NOTE: it won't work if the function repeatedly dies and gets created again, eg. +// a function defined inside forEach(function(...){...}) can't have caching inside +_global.cached = function(whatToCall, interval, notes) { + if (!defined(arguments.callee.caller.cachedTimes)) { + arguments.callee.caller.cachedTimes = {}; + arguments.callee.caller.cachedValues = {}; + } + var t = arguments.callee.caller.cachedTimes[notes]; + if (!defined(t) || gameTime - t >= interval) { + arguments.callee.caller.cachedValues[notes] = whatToCall(); + arguments.callee.caller.cachedTimes[notes] = gameTime; + } + return arguments.callee.caller.cachedValues[notes]; +} + +// if you actually want your script to send debug messages, consider using this function. +// it will only output each message only once, so your debug log will be readable. +_global.niceDebug = function() { + var msg = me + ": " + Array.prototype.join.call(arguments, " "); + if (throttled(Infinity, msg)) + return; + debug(msg); +} + +// use this if you want to split a certain void function into parts. +// example: +// function first() { return random(2); } +// function second() { return random(2); } +// function third() { return random(2); } +// function doManyThings() { +// if (functionSeries("doStuff", [ first, second, third ])) +// queue("doManyThings"); +// } +// NOTE: in this example functions of the list don't need to be global, but +// doManyThings does, otherwise the queue call will fail. +_global.functionSeries = function(id, list) { + if (!defined(functionSeries.last)) + functionSeries.last = {}; + if (!defined(functionSeries.last[id])) + functionSeries.last[id] = 0; + else + ++functionSeries.last[id]; + if (functionSeries.last[id] >= list.length) { + functionSeries.last[id] = 0; + return false; // all functions in the list were called, none succeeded + } + if (!list[functionSeries.last[id]]()) + return true; // none of the function succeeded yet, telling to call us again next time + functionSeries.last[id] = undefined; + return false; // one of the functions succeeded, no need to call us anymore +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/lassat.js b/har_includes/lassat.js new file mode 100644 index 0000000..6e5b015 --- /dev/null +++ b/har_includes/lassat.js @@ -0,0 +1,30 @@ + +/* + * This file is responsible for incoming laser satellite strikes. + * Relies on lassatSplash variable of the ruleset to figure out + * the radius of lassat. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// pick a target and fire +_global.fireLassat = function(structure) { + list = []; + enumLivingPlayers().filter(isEnemy).forEach(function(i) { + list = list.concat(enumStruct(i), enumDroid(i)); + }); + var maxIdx, maxPrice = 0; + list.forEach(function(obj, idx) { + var price = enumRange(obj.x, obj.y, lassatSplash / 2, ENEMIES, false).reduce(function(prev, curr) { return prev + curr.cost; }, 0); + if (price > maxPrice) { + maxPrice = price; + maxIdx = idx; + } + }); + activateStructure(structure, list[maxIdx]); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/math.js b/har_includes/math.js new file mode 100644 index 0000000..169398e --- /dev/null +++ b/har_includes/math.js @@ -0,0 +1,149 @@ + +/* + * This file includes routines and algorithms completely unrelated to Warzone 2100. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// return random true or false in certain chance +// 0 will return true with chance 0% +// 50 will return true with chance 50% +// 100 will return true with chance 100% +_global.withChance = function distance(chancePercent) { + return 1 + random(100) <= chancePercent; +} + +// Get distance between two points +// acceptable arguments: +// distance(obj, obj) +// distance(x,y, obj) +// distance(obj,x,y) +_global.distance = function distance(obj1, obj2, obj3, obj4) { + var x1, x2, y1, y2; + if (defined(obj1.x)) { + x1 = obj1.x; + y1 = obj1.y; + if (defined(obj2.x)) { + x2 = obj2.x; + y2 = obj2.y; + } else { + x2 = obj2; + y2 = obj3; + } + } else { + x1 = obj1; + y1 = obj2; + if (defined(obj3.x)) { + x2 = obj3.x; + y2 = obj3.y; + } else { + x2 = obj3; + y2 = obj4; + } + } + return distBetweenTwoPoints(x1, y1, x2, y2); +} + +_global.defined = function(variable) { + return typeof(variable) !== "undefined"; +} + +// returns a random number between 0 and max-1 inclusively +_global.random = function(max) { + if (max > 0) + return Math.floor(Math.random() * max); +} + +// some useful array functions +Array.prototype.random = function() { + if (this.length > 0) + return this[random(this.length)]; +} + +Array.prototype.last = function() { + if (this.length > 0) + return this[this.length - 1]; +} + +Array.prototype.filterProperty = function(property, value) { + return this.filter(function(element) { + return element[property] === value; + }); +} + +Array.prototype.someProperty = function(property, value) { + return this.some(function(element) { + return element[property] === value; + }); +} + +Array.prototype.shuffle = function() { + return this.sort(function() { return 0.5 - Math.random(); }) +} + +_global.zeroArray = function(l) { + var ret = []; + for (var i = 0; i < l; ++i) + ret[i] = 0; + return ret; +} + +_global.randomUnitArray = function(l) { + var ret = zeroArray(l); + ret[random(l)] = 1; + return ret; +} + +Array.prototype.addArray = function(arr) { + for (var i = 0; i < this.length; ++i) + this[i] += arr[i]; +} + +// returns a random property of an object +_global.randomItem = function(obj) { + var ret; + var count = 0; + for (var i in obj) + if (Math.random() < 1/++count) + ret = i; + return obj[ret]; +} + +// cluster analysis happens here +_global.naiveFindClusters = function(list, size) { + var ret = { clusters: [], xav: [], yav: [], maxIdx: 0, maxCount: 0 }; + for (var i = list.length - 1; i >= 0; --i) { + var x = list[i].x, y = list[i].y; + var found = false; + for (var j = 0; j < ret.clusters.length; ++j) { + if (distance(ret.xav[j], ret.yav[j], x, y) < size) { + var n = ret.clusters[j].length; + ret.clusters[j][n] = list[i]; + ret.xav[j] = (n * ret.xav[j] + x) / (n + 1); + ret.yav[j] = (n * ret.yav[j] + y) / (n + 1); + if (ret.clusters[j].length > ret.maxCount) { + ret.maxIdx = j; + ret.maxCount = ret.clusters[j].length; + } + found = true; + break; + } + } + if (!found) { + var n = ret.clusters.length; + ret.clusters[n] = [list[i]]; + ret.xav[n] = x; + ret.yav[n] = y; + if (1 > ret.maxCount) { + ret.maxIdx = n; + ret.maxCount = 1 + } + } + } + return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/misc.js b/har_includes/misc.js new file mode 100644 index 0000000..b5fbf87 --- /dev/null +++ b/har_includes/misc.js @@ -0,0 +1,56 @@ + +/* + * A few globals that didn't have a place for them in any other file + * + */ + +var baseLocation = startPositions[me]; + +function distanceToBase(loc) { + return distance(loc, baseLocation); +} + +function canReachFromBase(propulsion, location) { + if (defined(propulsion)) + return propulsionCanReach(propulsion, baseLocation.x, baseLocation.y, location.x, location.y); +} + +function myPower() { + return playerPower(me) - queuedPower(me); +} + +function isAvailable(stat) { + return isStructureAvailable(stat, me); +} + +function isAlly(player) { + return allianceExistsBetween(me, player); +} + +function isEnemy(player) { + return !isAlly(player); +} + +function enumLivingPlayers() { + function uncached() { + var ret = []; + for (var i = 0; i < maxPlayers; ++i) { + if (countStructList(targets, i) > 0) { + ret.push(i); + continue; + } + if (enumDroid(i).length > 0) + ret.push(i); + } + return ret; + }; + return cached(uncached, 30000); +} + +function enumTrucks() { + return enumDroid(me, DROID_CONSTRUCT); +} + +function goEasy() { + personality = randomItem(subpersonalities); +} diff --git a/har_includes/produce.js b/har_includes/produce.js new file mode 100644 index 0000000..c2638c7 --- /dev/null +++ b/har_includes/produce.js @@ -0,0 +1,239 @@ + +/* + * This file is responsible for droid production. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +function ourBuildDroid(factory, name, bodies, propulsions, weapons1, weapons2, weapons3) { + return buildDroid(factory, name, bodies, propulsions, "", "", weapons1, weapons2, weapons3); +} + +function produceTruck(factory, turrets) { + var turret = truckTurrets.concat(); + if (defined(turrets)) + turret = turrets.concat(); + turret.reverse(); + // TODO: switch to using chooseBodyWeaponPair() here + var bodies = filterBodyStatsByUsage(BODYUSAGE.TRUCK, BODYCLASS.KINETIC).map(function(val) { return val.stat; }); + var propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND|PROPULSIONUSAGE.HOVER); + return ourBuildDroid(factory, "Fancy Truck", bodies, propulsions, turret); +} + +function chooseWeapon(forVtol) { + if (!defined(forVtol)) + forVtol = false; + if (forVtol) { + var ret = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 3); + if (defined(ret)) + return ret.vtols.concat().reverse(); + } else { + var ret = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 0); + if (defined(ret)) + return ret.weapons.concat().reverse(); + } +} + +function chooseBodyWeaponPair(bodies, weapons) { + if (!defined(bodies)) + return undefined; + if (!defined(weapons)) + return undefined; + for (var i = 0; i < weapons.length; ++i) { + var w = weapons[i].stat, ww = weapons[i].weight; + if (!componentAvailable(w)) + continue; + for (var j = 0; j < bodies.length; ++j) { + var b = bodies[j].stat, bw = bodies[j].weight; + if (!componentAvailable(b)) + continue; + switch(ww) { + case WEIGHT.ULTRALIGHT: + if (bw <= WEIGHT.LIGHT) + return {b: b, w: w}; + break; + case WEIGHT.LIGHT: + if (bw <= WEIGHT.MEDIUM) + return {b: b, w: w}; + break; + case WEIGHT.MEDIUM: + return {b: b, w: w}; + break; + case WEIGHT.HEAVY: + if (bw >= WEIGHT.MEDIUM) + return {b: b, w: w}; + break; + case WEIGHT.ULTRAHEAVY: + if (bw >= WEIGHT.HEAVY) + return {b: b, w: w}; + break; + } + } + } +} + +function produceTank(factory) { + // TODO: needs refactoring. Make some more clever sorting. + var bodies = [] + if (chooseBodyClass() === BODYCLASS.KINETIC) { + bodies = bodies.concat( + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.KINETIC), + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.THERMAL) + ); + } else { + bodies = bodies.concat( + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.THERMAL), + filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.KINETIC) + ); + } + var propulsions; + var ret = scopeRatings(); + var rnd = random(ret.land + ret.sea); + if (!defined(rnd)) // we need only vtols? + return false; + propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND); + if (iHaveHover()) { + if (rnd >= ret.land) + propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER); + } else { + if (ret.land === 0) + return false; + } + var bwPair = chooseBodyWeaponPair(bodies, chooseWeapon()); + if (!defined(bwPair)) + return false; + return ourBuildDroid(factory, "Tank", bwPair.b, propulsions, bwPair.w, bwPair.w, bwPair.w); +} + +function produceVtol(factory) { + // TODO: consider thermal bodies + var bodies = filterBodyStatsByUsage(BODYUSAGE.AIR, BODYCLASS.KINETIC) + var propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.VTOL); + var bwPair = chooseBodyWeaponPair(bodies, chooseWeapon(true)); + if (!defined(bwPair)) + return false; + return ourBuildDroid(factory, "VTOL", bwPair.b, propulsions, bwPair.w, bwPair.w, bwPair.w); +} + +function produceTemplateFromList(factory, list) { + var ret = scopeRatings(); + for (var i = list.length - 1; i >= 0; --i) { + if (ret.land === 0 && !isHoverPropulsion(list[i].prop) && !isVtolPropulsion(list[i].prop)) + continue; + if (ret.land === 0 && ret.sea === 0 && !isVtolPropulsion(list[i].prop)) + continue; + if (isVtolPropulsion(list[i].prop) !== (factory.stattype === VTOL_FACTORY)) + continue; + if ((!randomTemplates) || withChance(100 / (i + 1))) + if (ourBuildDroid(factory, "Template Droid", list[i].body, list[i].prop, list[i].weapons[0], list[i].weapons[1], list[i].weapons[2])) + return true; + } + return false; +} + +function produceTemplate(factory) { + var path = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 1); + if (defined(path)) + return produceTemplateFromList(factory, path.templates); + return false; +} + +_global.checkTruckProduction = function() { + var trucks = enumTrucks(); + var hoverTrucksCount = trucks.filter(function(droid) { return isHoverPropulsion(droid.propulsion); }).length; + if (iHaveHover() && hoverTrucksCount < personality.minHoverTrucks) { + groundTrucks = trucks.filter(function(droid) { return !isHoverPropulsion(droid.propulsion); }); + if (groundTrucks.length > personality.minTrucks) { + groundTrucks.length -= personality.minTrucks; + groundTrucks.forEach(function(droid) { orderDroid(droid, DORDER_RECYCLE); }); + return false; + } + } + if (trucks.length >= getDroidLimit(me, DROID_CONSTRUCT)) + return false; + if (trucks.length < personality.minTrucks || myPower() > personality.maxPower + || (iHaveHover() && hoverTrucksCount < personality.minHoverTrucks) + ) { + var f; + f = enumFinishedStructList(structures.factories)[0]; + if (defined(f)) + if (structureIdle(f)) + if (produceTruck(f)) + return true; + if (defined(f)) + return false; + f = enumFinishedStructList(structures.templateFactories)[0]; + if (defined(f)) + if (structureIdle(f)) + if (produceTemplateFromList(f, truckTemplates)) + return true; + } + if (!iHaveArty()) + return false; + var sensors = enumDroid(me, DROID_SENSOR).length; + if (withChance(100 - 100 * sensors / personality.maxSensors)) { + f = enumFinishedStructList(structures.factories)[0]; + if (defined(f)) + if (structureIdle(f)) + if (produceTruck(f, sensorTurrets)) + return true; + } + return false; +} + +function checkTankProduction() { + if (!iCanDesign()) + return false; // don't cheat by producing tanks before design is available (also saves money for early generators) + var success = false; + enumIdleStructList(structures.factories).forEach(function(factory) { + success = success || produceTank(factory); + }); + return success; +} + +function checkTemplateProduction() { + var success = false; + enumIdleStructList(structures.templateFactories) + .concat(enumIdleStructList(structures.vtolFactories)) + .forEach(function(factory) + { + success = success || produceTemplate(factory); + }); + return success; +} + +function checkVtolProduction() { + var success = false; + if (!iCanDesign()) + return false; // don't cheat by producing vtols before design is available + enumIdleStructList(structures.vtolFactories).forEach(function(factory) { + success = success || produceVtol(factory); + }); + return success; +} + +_global.checkProduction = function() { + switch(chooseObjectType()) { + case 1: + if (checkTemplateProduction()) + return; + case 3: + if (checkVtolProduction()) + return; + default: + if (checkTankProduction()) + return; + } + // if having too much energy, don't care about what we produce + if (myPower() > personality.maxPower) { + queue("checkConstruction"); + checkTemplateProduction(); + checkTankProduction(); + checkVtolProduction(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/research.js b/har_includes/research.js new file mode 100644 index 0000000..08f8e4a --- /dev/null +++ b/har_includes/research.js @@ -0,0 +1,78 @@ + +/* + * This file controls the AI's research choices. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +var forcedResearch; + +_global.setForcedResearch = function(list) { + forcedResearch = list; +} + +function doResearch(lab) { + if (defined(forcedResearch)) { + if (forcedResearch === null) + return false; + if (pursueResearch(lab, forcedResearch)) + return true; + } + // if we need to quickly get a certain propulsion to reach the enemy, prioritize that. + var ret = scopeRatings(); + if (ret.land === 0 && ret.sea === 0 && !iHaveVtol()) + if (pursueResearch(lab, [ + propulsionStatsToResList(PROPULSIONUSAGE.VTOL), + fastestResearch, + ].random())) + return true; + if (ret.land === 0 && ret.sea !== 0 && !iHaveHover() && !iHaveVtol()) + if (pursueResearch(lab, [ + propulsionStatsToResList(PROPULSIONUSAGE.HOVER), + propulsionStatsToResList(PROPULSIONUSAGE.VTOL), + fastestResearch, + ].random())) + return true; + // otherwise, start with completing the fixed research path + if (defined(personality.earlyResearch)) + if (pursueResearch(lab, personality.earlyResearch)) + return true; + // then, see if we want to research some weapons + var objType = chooseObjectType(); + if (withChance(70)) { // TODO: make a more thoughtful decision here + var list = weaponStatsToResList(chooseAvailableWeaponPathByRoleRatings(personality.weaponPaths, chooseWeaponRole()), objType); + if (pursueResearch(lab, list)) + return true; + } + if (withChance(65)) { // TODO: make a more thoughtful decision here + if (chooseBodyClass() === BODYCLASS.KINETIC) { + if (withChance(40)) + if (pursueResearch(lab, classResearch.kinetic[objType])) + return true; + if (objType === OBJTYPE.TANK || objType === OBJTYPE.VTOL || (objType === OBJTYPE.DEFS && personality.defensiveness < 100)) + if (pursueResearch(lab, bodyStatsToResList(BODYCLASS.KINETIC))) + return true; + } else { + if (withChance(40)) + if (pursueResearch(lab, classResearch.thermal[objType])) + return true; + if (objType === OBJTYPE.TANK || objType === OBJTYPE.VTOL || (objType === OBJTYPE.DEFS && personality.defensiveness < 100)) + if (pursueResearch(lab, bodyStatsToResList(BODYCLASS.THERMAL))) + return true; + } + } + // if nothing of the above holds, do some generic research + if (pursueResearch(lab, fundamentalResearch)) + return true; + return false; + +} + +_global.checkResearch = function() { + enumIdleStructList(structures.labs).forEach(doResearch); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/stats.js b/har_includes/stats.js new file mode 100644 index 0000000..b970904 --- /dev/null +++ b/har_includes/stats.js @@ -0,0 +1,233 @@ + +/* + * This file contain functions for manipulating stats defined by rulesets. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +_global.isHoverPropulsion = function(str) { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.HOVER && val.stat === str; }); +} + +_global.isVtolPropulsion = function(str) { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.VTOL && val.stat === str; }); +} + +_global.iHaveHover = function() { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.HOVER && componentAvailable(val.stat);} ); +} + +_global.iHaveVtol = function() { + return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.VTOL && componentAvailable(val.stat);} ); +} + +_global.iHaveArty = function() { + for (var stat in weaponStats) + for (var i = 0; i < weaponStats[stat].defenses.length; ++i) + if (weaponStats[stat].defenses[i].defrole === DEFROLE.ARTY) + if (countStruct(weaponStats[stat].defenses[i].stat) > 0) + return true; + return false; +} + +// works with stored droid objects too! +_global.safeIsVtol = function(droid) { + return isVtolPropulsion(droid.propulsion); +} + +_global.enumStructList = function(list, player) { + if (!defined(player)) + player = me; + return list.reduce(function(summ, new_value) { return summ.concat(enumStruct(player, new_value)); }, []); +} + +_global.countStructList = function(list, player) { + if (!defined(player)) + player = me; + return list.reduce(function(summ, new_value) { return summ + countStruct(new_value, player); }, 0); +} + +_global.enumFinishedStructList = function(list, player) { + return enumStructList(list, player).filterProperty("status", BUILT); +} + +_global.countFinishedStructList = function(list, player) { + return enumFinishedStructList(list, player).length; +} + +_global.enumIdleStructList = function(list, player) { + return enumFinishedStructList(list, player).filter(structureIdle); +} + +_global.structListLimit = function(list) { + return list.reduce(function (summ, val) {return summ + getStructureLimit(val)}, 0) +} + +_global.guessWeaponRole = function (name) { + for (var stat in weaponStats) { + if ( + weaponStats[stat].weapons.someProperty("stat", name) || + weaponStats[stat].vtols.someProperty("stat", name) || + weaponStats[stat].templates.some(function (i) {return i.weapons.indexOf(name) > -1;}) + ) + return weaponStats[stat].roles; + } + niceDebug("Ruleset warning: Couldn't guess weapon role of", name); +} + +function guessWeaponMicro(name) { + function uncached() { + for (var stat in weaponStats) + if (weaponStats[stat].weapons.someProperty("stat", name)) + return weaponStats[stat].micro; + if (weaponStats[stat].templates.some(function(i) { return i.weapons.indexOf(name) > -1; })) + return weaponStats[stat].micro; + } + return cached(uncached, Infinity, name); +} + +_global.guessDroidMicro = function(droid) { + for (var i = 0; i < droid.weapons.length; ++i) { + var ret = guessWeaponMicro(droid.weapons[i].name); + if (ret !== MICRO.RANGED) + return ret; + } + return MICRO.RANGED; +} + +_global.guessBodyArmor = function(name) { + var body = bodyStats.filterProperty("stat", name).last() + if (defined(body)) + return body.armor; + else + niceDebug("Ruleset warning: Couldn't guess body class of", name); +} + +function weaponPathIsAvailable(path, objectType, defrole) { + switch(objectType) { + case 0: + return path.weapons.some(function(val) { return componentAvailable(val.stat); }) + case 1: + return path.templates.some(function(val) { + for (var i = 0; i < val.weapons.length; ++i) + if (!componentAvailable(val.weapons[i])) + return false; + return componentAvailable(val.body) && componentAvailable(val.prop); + }); + case 2: + return path.defenses.some(function(val) { return val.defrole === defrole && isAvailable(val.stat); }); + case 3: + return path.vtols.some(function(val) { return componentAvailable(val.stat); }); + default: // research + return true; + } +} + +_global.getProductionPaths = function() { + if (!defined(fallbackWeapon) || gameTime > 600000) + return personality.weaponPaths; + return [weaponStats[fallbackWeapon]].concat(personality.weaponPaths); +} + +_global.chooseAvailableWeaponPathByRoleRatings = function(paths, rating, objectType, defrole) { + var minDist = Infinity, minPath; + paths.forEach(function(path) { + if (!weaponPathIsAvailable(path, objectType, defrole)) + return; + var dist = 0; + for (var i = 0; i < ROLE.LENGTH; ++i) { + var newDist = Math.abs(rating[i] - path.roles[i]) + if (newDist > dist) + dist = newDist; + } + if (dist < minDist) { + minDist = dist; + minPath = path; + } + }); + return minPath; +} + +// +// here be functions for querying out research paths +// + +function statsToResList(path) { + return path.map(function (val) { return val.res; }); +} + +// todo make one StatsToResList and do filtering for path outside +_global.bodyStatsToResList = function(armor) { + return statsToResList(filterBodyStatsByUsage(armor)).reverse(); +} + +_global.propulsionStatsToResList = function(usage) { + return statsToResList(filterDataByFlag(propulsionStats, 'usage', usage)); +} + +_global.weaponStatsToResList = function(path, objType) { + if (!defined(path)) + return []; + var ret = []; + switch(objType) { + case 0: + ret = statsToResList(path.weapons); break; + case 1: + ret = statsToResList(path.templates); break; + case 2: + ret = statsToResList(path.defenses); break; + case 3: + ret = statsToResList(path.vtols); break; + } + if (ret.length === 0) + ret = ret.concat( + statsToResList(path.weapons), + statsToResList(path.templates), + statsToResList(path.defenses), + statsToResList(path.vtols) + ); + return ret; +} + +// +// here be functions for querying out component lists +// + +// TODO: move this to math. If we have same structure of data we can use it all around. +_global.filterDataByFlag = function(data, attr_name, flag) { + return data.filter(function(obj) { return obj[attr_name] & flag; }); +} + +_global.filterBodyStatsByUsage = function(usage, armor) { + var data; + if (defined(armor)) + data = filterDataByFlag(bodyStats, 'armor', armor) + else + data = bodyStats; + return filterDataByFlag(data, 'usage', usage).reverse(); +} + +_global.getPropulsionStatsComponents = function(usage) { + var data = filterDataByFlag(propulsionStats, 'usage', usage) + return data.map(function(val) { return val.stat; }).reverse() +} + +// +// here be functions for querying out defensive structures +// + +_global.weaponStatsToDefenses = function(stats, defrole) { + if (!defined(stats)) + return []; + var ret = []; + for (var i = 0; i < stats.defenses.length; ++i) + if (!defined(defrole) || stats.defenses[i].defrole === defrole) + ret.push(stats.defenses[i].stat); + // reverse not needed here + return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/tactics.js b/har_includes/tactics.js new file mode 100644 index 0000000..6d1abe7 --- /dev/null +++ b/har_includes/tactics.js @@ -0,0 +1,462 @@ + +/* + * This file is responsible for moving combat unit around. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +_global.MAX_GROUPS = maxPlayers; +_global.miscGroup = MAX_GROUPS; +_global.vtolGroup = miscGroup + 1; +var groupInfo = []; +var firstTimeHarass = true; + +function GroupInfo() { + this.lastAttacked = undefined; // gameTime at the moment of the last combat +} + +function safeGetObject(label) { + var obj = getObject(label); + if (obj === null) + return undefined; + return obj; +} + +function groupsBySize() { + var ret = []; + for (var i = 0; i < MAX_GROUPS; ++i) + if (isEnemy(i)) + ret.push(i); + ret.sort(function(one, two) { return groupSize(two) - groupSize(one); }); + return ret; +} + +function findLargestGroupIn(list) { + var sizes = [] + for (var i = 0; i < MAX_GROUPS; ++i) + sizes[i] = 0; + list.forEach(function(object) { + if (!defined(object.group) || object.group < 0 && object.group >= MAX_GROUPS) + return; + ++sizes[object.group]; + }); + var maxCount = 0, maxIdx = 0; + for (var i = 0; i < sizes.length; ++i) + if (sizes[i] > maxCount) { + maxCount = sizes[i]; + maxIdx = i; + } + return maxIdx; +} + +function findNearestGroup(x, y) { + var ret = naiveFindClusters(enumDroid(me).filter(function(droid) { + return !isVTOL(droid) && (droid.droidType === DROID_WEAPON || droid.droidType === DROID_CYBORG); + }), baseScale / 3); + if (ret.maxCount === 0) + return undefined; + var minDist = Infinity, minIdx; + var gr = []; + for (var i = 0; i < ret.clusters.length; ++i) { + gr[i] = findLargestGroupIn(ret.clusters[i]); + if (groupSize(gr[i]) > attackGroupSize()) { + var dist = distance(ret.xav[i], ret.yav[i], x, y); + if (dist < minDist) { + minDist = dist; + minIdx = i; + } + } + } + if (defined(minIdx)) + return gr[minIdx]; + gr = groupsBySize(); + if (gr.length > 0) + if (groupSize(gr[0]) > attackGroupSize()) + return gr[0]; +} + +function targetSuitableForHarass(object) { + function uncached() { + var ret = enumRange(object.x, object.y, baseScale / 2, ENEMIES, false).filter(function(obj) { + return !(obj.type == STRUCTURE && obj.stattype != DEFENSE); + }).length; + return ret <= groupSize(miscGroup); + } + return cached(uncached, 60000, object.id); +} + +function getGroupInfo(gr) { + if (defined(groupInfo[gr])) + return groupInfo[gr]; + groupInfo[gr] = new GroupInfo(); + return groupInfo[gr]; +} + +function groupTargetLabel(gr) { + return "NullBot_" + me + "_GroupTarget_" + gr; +} + +function groupMicroTargetLabel(gr) { + return "NullBot_" + me + "_GroupMicroTarget_" + gr; +} + +function vtolTargetLabel() { + return "NullBot_" + me + "_VtolTarget"; +} + +function findTarget(gr) { + var obj = safeGetObject(groupMicroTargetLabel(gr)); + var obj2 = safeGetObject(groupTargetLabel(gr)); + getGroupInfo(gr); + if (gameTime > groupInfo[gr].lastAttacked + 10000 && defined(obj2) && obj2.type === POSITION) + removeLabel(groupMicroTargetLabel(gr)); + if (!defined(obj)) + obj = obj2; + if (defined(obj)) { + if (gr === miscGroup) { + // harass group should switch targets when its target gets protected, otherwise targets are permanent + if (throttled(10000) || targetSuitableForHarass(obj)) + return obj; + } else + return obj; + } + // find harass targets for the misc group + if (gr === miscGroup) { + var list = enumStructList(miscTargets, enumLivingPlayers().filter(isEnemy).random()); + powerUps.forEach(function(stat) { // pick up oil drums and artifacts + list = list.concat(enumFeature(-1, stat)); + }); + list = list.filter(targetSuitableForHarass).filter(function(feature) { + if (iHaveHover()) + if (canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER)[0], feature)) + return true; + return canReachFromBase(getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND)[0], feature); + }).sort(function(one, two) { + return distanceToBase(one) - distanceToBase(two); + }); + obj = list[random(Math.min(3, list.length))]; + if (obj) { + addLabel(obj, groupTargetLabel(gr)); + return obj; + } + } + // fund structure targets + var list = enumStructList(targets, gr); + if (list.length > 0) + obj = list.random(); + else { + // find remaining droids + list = enumDroid(gr); + if (list.length > 0) + obj = list.random(); + } + if (defined(obj)) { + addLabel(obj, groupTargetLabel(gr)); + return obj; + } +} + +function groupInDanger(gr) { + getGroupInfo(gr); // make sure everything is defined + if (!defined(groupInfo[gr].lastAttacked)) + return false; + return gameTime - groupInfo[gr].lastAttacked < 10000; +} + +function regroup(gr) { + if (inPanic()) + return enumGroup(gr).filter(checkRepaired); + var size = attackGroupSize(); + if (size < groupSize(gr) / 2) + size = groupSize(gr) / 2; + var ret = naiveFindClusters(enumGroup(gr).filter(checkRepaired), (baseScale / 3)); + if (ret.maxCount === 0) + return []; + for (var i = 0; i < ret.clusters.length; ++i) + if (i !== ret.maxIdx) + for (var j = 0; j < ret.clusters[i].length; ++j) + orderDroidLoc(ret.clusters[i][j], DORDER_MOVE, ret.xav[ret.maxIdx], ret.yav[ret.maxIdx]); + if (ret.maxCount < size) { + for (var j = 0; j < ret.clusters[ret.maxIdx].length; ++j) { + if (groupInDanger(gr)) + orderDroid(ret.clusters[ret.maxIdx][j], DORDER_RTB); + else + orderDroid(ret.clusters[ret.maxIdx][j], DORDER_STOP); + } + return []; + } + return ret.clusters[ret.maxIdx]; +} + +function weHaveRepair() { + function uncached() { + return enumStruct(me, REPAIR_FACILITY).length > 0; + } + return cached(uncached, 1000); +} + +function checkRepaired(droid) { + if (!weHaveRepair()) + return true; + if (droid.order === DORDER_RTR) + return false; + if (droid.health < personality.repairAt) { + orderDroid(droid, DORDER_RTR); + return false; + } + return true; +} + +function droidFree(droid) { + if (droid.order === DORDER_SCOUT) + return false; + if (droid.order === DORDER_ATTACK) + return false; + if (droid.order === DORDER_RTR) + return false; + if (!checkRepaired(droid)) + return false; + return true; +} + +function attackTarget(droid) { + var target = findTarget(droid.group); + if (droid.group !== miscGroup) + if (!defined(target) || !droidCanReach(droid, target.x, target.y)) { + groupDroid(droid); + return; + } + if (defined(target)) + switch (target.type) { + case DROID: + if (droid.droidType !== DROID_SENSOR) + orderDroidObj(droid, DORDER_ATTACK, target); + else + orderDroidObj(droid, DORDER_OBSERVE, target); + break; + case FEATURE: + orderDroidObj(droid, DORDER_RECOVER, target); + break; + case STRUCTURE: + if (droid.droidType !== DROID_SENSOR) + orderDroidLoc(droid, DORDER_SCOUT, target.x, target.y); + else + orderDroidObj(droid, DORDER_OBSERVE, target); + break; + default: + orderDroidLoc(droid, DORDER_SCOUT, target.x, target.y); + break; + } +} + +function vtolCanHit(droid, obj) { + if (typeof(obj) === DROID && isVTOL(obj)) + return droid.canHitAir; + else + return droid.canHitGround; +} + +function pickVtolTarget(droid) { + function uncached() { + function canHit(obj) { + return vtolCanHit(droid, obj); + } + var enemy = enumLivingPlayers().filter(isEnemy).random(); + var list; + list = enumStructList(miscTargets, enemy).filter(canHit); + if (list.length > 0) return list.random(); + list = enumStruct(enemy, DEFENSE).filterProperty("canHitAir", true).filter(canHit); + if (list.length > 0) return list.random(); + list = enumDroid(enemy, DROID_WEAPON).filterProperty("canHitAir", true).filter(canHit); + if (list.length > 0) return list.random(); + list = enumDroid(enemy, DROID_CYBORG).filterProperty("canHitAir", true).filter(canHit); + if (list.length > 0) return list.random(); + list = enumStructList(targets, enemy).filter(canHit); + if (list.length > 0) return list.random(); + list = enumDroid(enemy).filter(canHit); + if (list.length > 0) return list.random(); + } + return cached(uncached, 100, droid.canHitAir + 2 * droid.canHitGround); +} + +function vtolArmed(obj, percent) { + if (obj.type != DROID) + return; + if (!isVTOL(obj)) + return false; + for (var i = 0; i < obj.weapons.length; ++i) + if (obj.weapons[i].armed >= 99) + return true; + return false; +} + +function vtolReady(droid) { + if (droid.order == DORDER_ATTACK) + return false; + if (vtolArmed(droid, 99)) + return true; + if (droid.order != DORDER_REARM) { + orderDroid(droid, DORDER_REARM); + buildVtols() // actually pads + } + return false; +} + +_global.attackGroupSize = function() { + var ret = personality.minTanks + (gameTime / 300000) * personality.becomeHarder; + if (ret > personality.maxTanks) + ret = personality.maxTanks; + return ret; +} + +_global.setTarget = function(object, group) { + if (!defined(group)) { + group = findNearestGroup(object.x, object.y); + if (object.type === STRUCTURE || object.type === DROID) + addLabel(object, vtolTargetLabel()); + } + if (!defined(group)) + return false; + if (defined(safeGetObject(groupTargetLabel(group)))) + if (throttled(10000, group)) // don't switch targets too often + return false; + if (object.type === DROID || (object.type === STRUCTURE && object.stattype === DEFENSE)) + addLabel(object, groupMicroTargetLabel(group)); + else + addLabel(object, groupTargetLabel(group)); + return true; +} + +_global.unsetTarget = function(player) { + for (var i = 0; i <= MAX_GROUPS; ++i) { + var obj = safeGetObject(groupTargetLabel(i)); + if (defined(obj) && obj.type === POSITION && findBeaconPlayer(obj.x, obj.y) === player) + removeLabel(groupTargetLabel(i)); + } +} + +_global.groupDroid = function(droid) { + if (droid.droidType === DROID_WEAPON || droid.droidType === DROID_CYBORG) { + if (isVTOL(droid)) { + groupAdd(vtolGroup, droid); + return; + } + if (withChance(100 - groupSize(miscGroup) * 50 / personality.maxMiscTanks) || firstTimeHarass) { + firstTimeHarass = false; + groupAdd(miscGroup, droid); + return; + } + var grp = groupsBySize().filter(function(i) { + if (isAlly(i)) + return false; + if (!defined(findTarget(i))) + return false; + if (!droidCanReach(droid, findTarget(i).x, findTarget(i).y)) + return false; + return true; + }); + var ret = grp.filter(function(i) { + return groupSize(i) < attackGroupSize() * 2 && defined(findTarget(i)); + }); + if (ret.length === 0) + ret = grp; + if (ret.length === 0) + ret = [ miscGroup ]; + groupAdd(ret[0], droid); + } + if (droid.droidType === DROID_SENSOR) + groupAdd(miscGroup, droid); +} + +_global.rebalanceGroups = function() { + if (throttled(5000)) + return; + if (groupSize(miscGroup) > personality.maxMiscTanks) { + var list = enumGroup(miscGroup).shuffle(); + for (var i = personality.maxMiscTanks; i < personality.maxMiscTanks + 5 && i < list.length; ++i) + groupDroid(list[i]); + } + var ret = groupsBySize(); + if (ret.length > 0) + if (ret[0] > 0 && ret[0] < attackGroupSize()) + for (var i = 1; i < ret.length; ++i) { + var list = enumGroup(ret[i]); + for (var j = 0; j < list.length; ++j) { + target = findTarget(ret[0]); + if (defined(target)) + if (droidCanReach(list[j], target.x, target.y)) { + groupAdd(ret[0], list[j]); + return; + } + } + } +} + +_global.touchGroup = function(gr) { + getGroupInfo(gr); // make sure everything is defined + groupInfo[gr].lastAttacked = gameTime; +} + +_global.fallBack = function(droid, threat) { + if (droid.order === DORDER_MOVE) + return; + if (guessDroidMicro(droid) === MICRO.MELEE) + return; + var x = droid.x - (baseScale / 7) * (threat.x - droid.x) / distance(threat, droid); + var y = droid.y - (baseScale / 7) * (threat.y - droid.y) / distance(threat, droid); + if (x < 1 || y < 1 || x > mapWidth - 2 || y > mapHeight - 2) + return; + orderDroidLoc(droid, DORDER_MOVE, x, y); +} + +_global.dangerLevel = function(loc) { + return enumRange(loc.x, loc.y, baseScale / 2, ENEMIES, false).length; +} + +_global.checkAttack = function() { + for (var i = 0; i < MAX_GROUPS; ++i) + if (!throttled(3000, i)) { + regroup(i).forEach(attackTarget); + break; + } + if (throttled(1000, "misc")) + return; + enumGroup(miscGroup).filter(checkRepaired).forEach(attackTarget); + if (throttled(5000, "vtols")) + return; + var droids = enumGroup(vtolGroup).filter(vtolReady); + if (droids.length > attackGroupSize() / 3.) + droids.forEach(function(droid) { + var target = pickVtolTarget(droid); + if (defined(target)) + orderDroidObj(droid, DORDER_ATTACK, target); + }); +} + +_global.pushVtols = function(object) { + var vtols = enumRange(object.x, object.y, 20, me, false); + var enemies = enumRange(object.x, object.y, 8, ENEMIES, true); + for (var i = 0; i < vtols.length; ++i) + if (vtolArmed(vtols[i], 1)) + for (var j = 0; j < enemies.length; ++j) + if (vtolCanHit(vtols[i], enemies[j])) { + orderDroidObj(vtols[i], DORDER_ATTACK, enemies[j]); + break; + } +} + +_global.inPanic = function() { + function uncached() { + var badGuys = enumRange(baseLocation.x, baseLocation.y, baseScale, ENEMIES).length; + var goodGuys = enumRange(baseLocation.x, baseLocation.y, baseScale, ALLIES).filter(function(object) { + return object.type === DROID && (object.droidType === DROID_WEAPON || object.droidType === DROID_CYBORG); + }).length; + return 3 * badGuys > 2 * goodGuys; + } + return cached(uncached, 10000); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_includes/timers.js b/har_includes/timers.js new file mode 100644 index 0000000..ec3f66e --- /dev/null +++ b/har_includes/timers.js @@ -0,0 +1,26 @@ + +/* + * This file lists all timers used by the AI. + * + */ + +(function(_global) { +//////////////////////////////////////////////////////////////////////////////////////////// + +// slightly shift all timers so that not to get too many of them on the same game frame, +// especially when multiple AI instances are running +function rnd() { + return random(201) - 100; +} + +_global.setTimers = function() { + setTimer("spendMoney", 2000 + 3 * rnd()); + setTimer("checkAttack", 100); + setTimer("adaptCycle", 100); + setTimer("rebalanceGroups", 10000 + 20 * rnd()); + if (difficulty === EASY) + setTimer("goEasy", 30000); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +})(this); diff --git a/har_rulesets/custom.js b/har_rulesets/custom.js new file mode 100644 index 0000000..977acfa --- /dev/null +++ b/har_rulesets/custom.js @@ -0,0 +1,564 @@ + +/* + * This file describes standard stats and strategies of + * the base (unmodded) game. + * + * If you want to make an AI specially designed for your mod, start by + * making a copy of this file and modifying it according to your mod's rules. + * + * Then provide a personality to use the ruleset, similar to + * how har_generic.[js|ai] is provided for this ruleset. + * + * You may find some useful functions for working with these stats + * in stats.js . + * + */ + +// a factor for figuring out how large things are in this ruleset, +// or simply a typical radius of a player's base +const baseScale = 20; + +// diameter of laser satellite splash/incendiary damage +// for use in lassat.js +const lassatSplash = 4; + +// set this to 1 to choose templates randomly, instead of later=>better. +const randomTemplates = 0; + +// this function is used for avoiding AI cheats that appear due to +// being able to build droids before designing them +function iCanDesign() { + if (difficulty === INSANE) // won't make INSANE much worse ... + return true; + return countFinishedStructList(structures.hqs) > 0; +} + +const structures = { + factories: [ "A0LightFactory", ], + templateFactories: [ "A0CyborgFactory", ], + vtolFactories: [ "A0VTolFactory1", ], + labs: [ "A0ResearchFacility", ], + gens: [ "A0PowerGenerator", ], + hqs: [ "A0CommandCentre", ], + vtolPads: [ "A0VtolPad", ], + derricks: [ "A0ResourceExtractor", ], + extras: [ "A0RepairCentre3", "A0Sat-linkCentre", "A0LasSatCommand", ], + sensors: [ "Sys-SensoTower02", "Sys-CB-Tower01", "Sys-RadarDetector01", "Sys-SensoTowerWS", ], +}; + +const oilResources = [ "OilResource", ]; + +const powerUps = [ "OilDrum", "Crate" ]; + +// NOTE: you cannot use specific stats as bases, but only stattypes +// probably better make use of .name rather than of .stattype here? +const modules = [ + { base: POWER_GEN, module: "A0PowMod1", count: 1, cost: MODULECOST.CHEAP }, + { base: FACTORY, module: "A0FacMod1", count: 2, cost: MODULECOST.EXPENSIVE }, + { base: VTOL_FACTORY, module: "A0FacMod1", count: 2, cost: MODULECOST.EXPENSIVE }, + { base: RESEARCH_LAB, module: "A0ResearchModule1", count: 1, cost: MODULECOST.EXPENSIVE }, +]; + +const targets = [] + .concat(structures.factories) + .concat(structures.templateFactories) + .concat(structures.vtolFactories) + .concat(structures.extras) +; + +const miscTargets = [] + .concat(structures.derricks) +; + +const sensorTurrets = [ + "SensorTurret1Mk1", // sensor + "Sensor-WideSpec", // wide spectrum sensor +]; + +const fundamentalResearch = [ + "R-Struc-PowerModuleMk1", + "R-Struc-RprFac-Upgrade01", + "R-Sys-Sensor-Tower02", + "R-Vehicle-Prop-Halftracks", + "R-Struc-Power-Upgrade01c", + "R-Vehicle-Prop-Tracks", + "R-Sys-CBSensor-Tower01", + "R-Struc-VTOLPad-Upgrade01", + "R-Struc-Power-Upgrade03a", + "R-Struc-VTOLPad-Upgrade03", + "R-Sys-Autorepair-General", + "R-Wpn-LasSat", + "R-Struc-RprFac-Upgrade04", + "R-Struc-VTOLPad-Upgrade06", + "R-Struc-RprFac-Upgrade06", +]; + +const fastestResearch = [ + "R-Struc-Research-Upgrade09", +]; + +// body and propulsion arrays don't affect fixed template droids +const bodyStats = [ + { res: "R-Vehicle-Body01", stat: "Body1REC", weight: WEIGHT.LIGHT, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.KINETIC }, // viper + { res: "R-Vehicle-Body05", stat: "Body5REC", weight: WEIGHT.MEDIUM, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.KINETIC }, // cobra + { res: "R-Vehicle-Body11", stat: "Body11ABT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // python + { res: "R-Vehicle-Body02", stat: "Body2SUP", weight: WEIGHT.LIGHT, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.KINETIC }, // leopard + { res: "R-Vehicle-Body06", stat: "Body6SUPP", weight: WEIGHT.MEDIUM, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.KINETIC }, // panther + { res: "R-Vehicle-Body09", stat: "Body9REC", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // tiger + { res: "R-Vehicle-Body13", stat: "Body13SUP", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // wyvern + { res: "R-Vehicle-Body14", stat: "Body14SUP", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // dragon + { res: "R-Vehicle-Body04", stat: "Body4ABT", weight: WEIGHT.LIGHT, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.THERMAL }, // bug + { res: "R-Vehicle-Body08", stat: "Body8MBT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.THERMAL }, // scorpion + { res: "R-Vehicle-Body12", stat: "Body12SUP", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.THERMAL }, // mantis + { res: "R-Vehicle-Body03", stat: "Body3MBT", weight: WEIGHT.MEDIUM, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.THERMAL }, // retaliation + { res: "R-Vehicle-Body07", stat: "Body7ABT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.THERMAL }, // retribution + { res: "R-Vehicle-Body10", stat: "Body10MBT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.THERMAL }, // vengeance +]; + +const classResearch = { + kinetic: [ + [ // OBJTYPE.TANK + "R-Vehicle-Metals09", + ], + [ // OBJTYPE.BORG + "R-Cyborg-Metals09", + ], + [ // OBJTYPE.DEFS + "R-Defense-WallUpgrade03", + "R-Struc-Materials03", + "R-Defense-WallUpgrade06", + "R-Struc-Materials06", + "R-Defense-WallUpgrade12", + "R-Struc-Materials09", + ], + [ // OBJTYPE.VTOL + "R-Vehicle-Metals09", + ], + ], + thermal: [ + [ + "R-Vehicle-Armor-Heat09", + ], + [ + "R-Cyborg-Armor-Heat09", + ], + [ + "R-Defense-WallUpgrade03", + "R-Struc-Materials03", + "R-Defense-WallUpgrade06", + "R-Struc-Materials06", + "R-Defense-WallUpgrade12", + "R-Struc-Materials09", + ], + [ + "R-Vehicle-Armor-Heat09", + ], + ], +} + +// NOTE: Please don't put hover propulsion into the ground list, etc.! +// NOTE: Hover propulsion should be placed AFTER ground propulsion! +// Adaptation code relies on that for discovering map topology. +// Ground propulsions need to be ground only, hover propulsions shouldn't +// be able to cross cliffs, but should be able to cross seas, etc. +const propulsionStats = [ + { res: "R-Vehicle-Prop-Wheels", stat: "wheeled01", usage: PROPULSIONUSAGE.GROUND }, + { res: "R-Vehicle-Prop-Halftracks", stat: "HalfTrack", usage: PROPULSIONUSAGE.GROUND }, + { res: "R-Vehicle-Prop-Tracks", stat: "tracked01", usage: PROPULSIONUSAGE.GROUND }, + { res: "R-Vehicle-Prop-Hover", stat: "hover01", usage: PROPULSIONUSAGE.HOVER }, + { res: "R-Vehicle-Prop-VTOL", stat: "V-Tol", usage: PROPULSIONUSAGE.VTOL }, +]; + + +const truckTurrets = [ + "Spade1Mk1", +]; + +const truckTemplates = [ + { body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgSpade", ] } // engineer +]; + +const fallbackWeapon = 'machineguns'; + +// Unlike bodies and propulsions, weapon lines don't have any specific meaning. +// You can make as many weapon lines as you want for your ruleset. +const weaponStats = { + machineguns: { + // How good weapons of this path are against tanks, borgs, defenses, vtols? + // The sum of the four should be equal to 1. + roles: [ 0.1, 0.7, 0.1, 0.1 ], + // This explains how are human players supposed to call this weapon path in the chat. + chatalias: "mg", + // This controls micromanagement of units based on the weapons of this path. + micro: MICRO.RANGED, + // Weapons of the path, better weapons below. + weapons: [ + { res: "R-Wpn-MG1Mk1", stat: "MG1Mk1", weight: WEIGHT.ULTRALIGHT }, // mg + { res: "R-Wpn-MG2Mk1", stat: "MG2Mk1", weight: WEIGHT.LIGHT }, // tmg + { res: "R-Wpn-MG3Mk1", stat: "MG3Mk1", weight: WEIGHT.MEDIUM }, // hmg + { res: "R-Wpn-MG4", stat: "MG4ROTARYMk1", weight: WEIGHT.MEDIUM }, // ag + { res: "R-Wpn-MG5", stat: "MG5TWINROTARY", weight: WEIGHT.MEDIUM }, // tag + ], + // VTOL weapons of the path, in the same order. + vtols: [ + { res: "R-Wpn-MG3Mk1", stat: "MG3-VTOL", weight: WEIGHT.ULTRALIGHT }, // vtol hmg + { res: "R-Wpn-MG4", stat: "MG4ROTARY-VTOL", weight: WEIGHT.MEDIUM }, // vtol ag + ], + // Defensive structures of the path, in the same order. + // NOTE: a defensive structure is recycled whenever there are at least two structures + // with the same role available down the list + defenses: [ + // turtle AI needs early versatile towers, hence duplicate stat + { res: "R-Defense-Tower01", stat: "GuardTower1", defrole: DEFROLE.GATEWAY }, // hmg tower + { res: "R-Defense-Tower01", stat: "GuardTower1", defrole: DEFROLE.STANDALONE }, // hmg tower + { res: "R-Defense-Pillbox01", stat: "PillBox1", defrole: DEFROLE.STANDALONE }, // hmg bunker + { res: "R-Defense-WallTower01", stat: "WallTower01", defrole: DEFROLE.GATEWAY }, // hmg hardpoint + { res: "R-Defense-RotMG", stat: "Pillbox-RotMG", defrole: DEFROLE.STANDALONE }, // ag bunker + { res: "R-Defense-Wall-RotMg", stat: "Wall-RotMg", defrole: DEFROLE.GATEWAY }, // ag hardpoint + { res: "R-Defense-WallTower-TwinAGun", stat: "WallTower-TwinAssaultGun", defrole: DEFROLE.GATEWAY }, // tag hardpoint + ], + // Cyborg templates, better borgs below, as usual. + templates: [ + { res: "R-Wpn-MG1Mk1", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgChaingun", ] }, // mg cyborg + { res: "R-Wpn-MG4", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgRotMG", ] }, // ag cyborg + ], + // Extra things to research on this path, even if they don't lead to any new stuff + extras: [ + "R-Wpn-MG-Damage08", + ], + }, + flamers: { + roles: [ 0.3, 0.7, 0.0, 0.0 ], + chatalias: "fl", + micro: MICRO.MELEE, + weapons: [ + { res: "R-Wpn-Flamer01Mk1", stat: "Flame1Mk1", weight: WEIGHT.LIGHT }, // flamer + { res: "R-Wpn-Flame2", stat: "Flame2", weight: WEIGHT.HEAVY }, // inferno + { res: "R-Wpn-Plasmite-Flamer", stat: "PlasmiteFlamer", weight: WEIGHT.HEAVY }, // plasmite + ], + vtols: [], + defenses: [ + { res: "R-Defense-Pillbox05", stat: "PillBox5", defrole: DEFROLE.GATEWAY }, // flamer bunker + { res: "R-Defense-HvyFlamer", stat: "Tower-Projector", defrole: DEFROLE.GATEWAY }, // inferno bunker + { res: "R-Defense-PlasmiteFlamer", stat: "Plasmite-flamer-bunker", defrole: DEFROLE.GATEWAY }, // plasmite bunker + ], + templates: [ + { res: "R-Wpn-Flamer01Mk1", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgFlamer01", ] }, // flamer cyborg + { res: "R-Wpn-Flame2", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Thermite", ] }, // flamer cyborg + ], + extras: [ + "R-Wpn-Flamer-ROF03", + "R-Wpn-Flamer-Damage09", + ], + }, + cannons: { + roles: [ 0.5, 0.2, 0.3, 0.0 ], + chatalias: "cn", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Cannon1Mk1", stat: "Cannon1Mk1", weight: WEIGHT.LIGHT }, // lc + { res: "R-Wpn-Cannon2Mk1", stat: "Cannon2A-TMk1", weight: WEIGHT.HEAVY }, // mc + { res: "R-Wpn-Cannon4AMk1", stat: "Cannon4AUTOMk1", weight: WEIGHT.HEAVY }, // hpv + { res: "R-Wpn-Cannon5", stat: "Cannon5VulcanMk1", weight: WEIGHT.HEAVY }, // ac + { res: "R-Wpn-Cannon6TwinAslt", stat: "Cannon6TwinAslt", weight: WEIGHT.ULTRAHEAVY }, // tac + { res: "R-Wpn-Cannon3Mk1", stat: "Cannon375mmMk1", weight: WEIGHT.ULTRAHEAVY }, // hc + { res: "R-Wpn-RailGun01", stat: "RailGun1Mk1", weight: WEIGHT.LIGHT }, // needle + { res: "R-Wpn-RailGun02", stat: "RailGun2Mk1", weight: WEIGHT.HEAVY }, // rail + { res: "R-Wpn-RailGun03", stat: "RailGun3Mk1", weight: WEIGHT.ULTRAHEAVY }, // gauss + ], + vtols: [ + { res: "R-Wpn-Cannon1Mk1", stat: "Cannon1-VTOL", weight: WEIGHT.LIGHT }, // lc + { res: "R-Wpn-Cannon4AMk1", stat: "Cannon4AUTO-VTOL", weight: WEIGHT.HEAVY }, // hpv + { res: "R-Wpn-Cannon5", stat: "Cannon5Vulcan-VTOL", weight: WEIGHT.HEAVY }, // ac + { res: "R-Wpn-RailGun01", stat: "RailGun1-VTOL", weight: WEIGHT.LIGHT }, // needle + { res: "R-Wpn-RailGun02", stat: "RailGun2-VTOL", weight: WEIGHT.HEAVY }, // rail + ], + defenses: [ + { res: "R-Defense-Pillbox04", stat: "PillBox4", defrole: DEFROLE.STANDALONE }, // lc bunker + { res: "R-Defense-WallTower02", stat: "WallTower02", defrole: DEFROLE.GATEWAY }, // lc hard + { res: "R-Defense-WallTower03", stat: "WallTower03", defrole: DEFROLE.GATEWAY }, // mc hard + { res: "R-Defense-Emplacement-HPVcannon", stat: "Emplacement-HPVcannon", defrole: DEFROLE.STANDALONE }, // hpv empl + { res: "R-Defense-WallTower-HPVcannon", stat: "WallTower-HPVcannon", defrole: DEFROLE.GATEWAY }, // hpv hard + { res: "R-Defense-Wall-VulcanCan", stat: "Wall-VulcanCan", defrole: DEFROLE.GATEWAY }, // ac hard + { res: "R-Defense-Cannon6", stat: "PillBox-Cannon6", defrole: DEFROLE.STANDALONE }, // tac bunker + { res: "R-Defense-WallTower04", stat: "WallTower04", defrole: DEFROLE.GATEWAY }, // hc hard + { res: "R-Defense-Super-Cannon", stat: "X-Super-Cannon", defrole: DEFROLE.FORTRESS }, // cannon fort + { res: "R-Defense-GuardTower-Rail1", stat: "GuardTower-Rail1", defrole: DEFROLE.STANDALONE }, // needle tower + { res: "R-Defense-Rail2", stat: "Emplacement-Rail2", defrole: DEFROLE.STANDALONE }, // rail empl + { res: "R-Defense-WallTower-Rail2", stat: "WallTower-Rail2", defrole: DEFROLE.GATEWAY }, // rail hard + { res: "R-Defense-Rail3", stat: "Emplacement-Rail3", defrole: DEFROLE.STANDALONE }, // gauss empl + { res: "R-Defense-WallTower-Rail3", stat: "WallTower-Rail3", defrole: DEFROLE.GATEWAY }, // gauss hard + { res: "R-Defense-MassDriver", stat: "X-Super-MassDriver", defrole: DEFROLE.FORTRESS }, // mass driver fort + ], + templates: [ + { res: "R-Wpn-Cannon1Mk1", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgCannon", ] }, // lc borg + { res: "R-Cyborg-Hvywpn-Mcannon", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-Mcannon", ] }, // mc super + { res: "R-Cyborg-Hvywpn-HPV", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-HPV", ] }, // hpv super + { res: "R-Cyborg-Hvywpn-Acannon", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-Acannon", ] }, // ac super + { res: "R-Wpn-RailGun01", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Rail1", ] }, // needle borg + { res: "R-Cyborg-Hvywpn-RailGunner", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-RailGunner", ] }, // rail super + ], + extras: [ + "R-Wpn-Cannon-ROF06", + "R-Vehicle-Engine09", // cannons are heeeeavy + "R-Wpn-Rail-Damage03", // sure it's required by gauss, but what if our AI uses only cyborgs and vtols? + "R-Wpn-Rail-ROF03", + ], + }, + cannons_AA: { + roles: [ 0.0, 0.0, 0.0, 1.0 ], + chatalias: "ca", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-AAGun02", stat: "AAGun2Mk1", weight: WEIGHT.HEAVY }, + ], + vtols: [], + defenses: [ + { res: "R-Defense-AASite-QuadBof", stat: "AASite-QuadBof", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-WallTower-DoubleAAgun", stat: "WallTower-DoubleAAGun", defrole: DEFROLE.GATEWAY }, + ], + templates: [], + extras: [], + }, + mortars: { + roles: [ 0.2, 0.4, 0.4, 0.0 ], + chatalias: "mo", + micro: MICRO.DUMB, + weapons: [ + { res: "R-Wpn-Mortar01Lt", stat: "Mortar1Mk1", weight: WEIGHT.HEAVY }, // duplicate stat! + { res: "R-Wpn-Mortar02Hvy", stat: "Mortar2Mk1", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-Mortar3", stat: "Mortar3ROTARYMk1", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-HowitzerMk1", stat: "Howitzer105Mk1", weight: WEIGHT.ULTRAHEAVY }, + { res: "R-Wpn-Howitzer03-Rot", stat: "Howitzer03-Rot", weight: WEIGHT.ULTRAHEAVY }, + { res: "R-Wpn-HvyHowitzer", stat: "Howitzer150Mk1", weight: WEIGHT.ULTRAHEAVY }, + ], + vtols: [ + { res: "R-Wpn-Bomb01", stat: "Bomb1-VTOL-LtHE", weight: WEIGHT.LIGHT }, + { res: "R-Wpn-Bomb02", stat: "Bomb2-VTOL-HvHE", weight: WEIGHT.HEAVY }, + ], + defenses: [ + { res: "R-Defense-MortarPit", stat: "Emplacement-MortarPit01", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-HvyMor", stat: "Emplacement-MortarPit02", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-RotMor", stat: "Emplacement-RotMor", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-Howitzer", stat: "Emplacement-Howitzer105", defrole: DEFROLE.ARTY }, + { res: "R-Defense-RotHow", stat: "Emplacement-RotHow", defrole: DEFROLE.ARTY }, + { res: "R-Defense-HvyHowitzer", stat: "Emplacement-Howitzer150", defrole: DEFROLE.ARTY }, + ], + templates: [ + { res: "R-Wpn-Mortar01Lt", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Grenade", ] }, + ], + extras: [ + "R-Wpn-Bomb-Accuracy03", + "R-Wpn-Howitzer-Damage06", + "R-Wpn-Howitzer-ROF04", + "R-Wpn-Howitzer-Accuracy03", + ], + }, + fireMortars: { + roles: [ 0.3, 0.3, 0.4, 0.0 ], + chatalias: "fm", + micro: MICRO.DUMB, + weapons: [ + { res: "R-Wpn-Mortar01Lt", stat: "Mortar1Mk1", weight: WEIGHT.HEAVY }, // duplicate stat! + { res: "R-Wpn-Mortar-Incenediary", stat: "Mortar-Incenediary", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-Howitzer-Incenediary", stat: "Howitzer-Incenediary", weight: WEIGHT.ULTRAHEAVY }, + ], + vtols: [ + { res: "R-Wpn-Bomb03", stat: "Bomb3-VTOL-LtINC", weight: WEIGHT.LIGHT }, + { res: "R-Wpn-Bomb04", stat: "Bomb4-VTOL-HvyINC", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-Bomb05", stat: "Bomb5-VTOL-Plasmite", weight: WEIGHT.HEAVY }, + ], + defenses: [ + { res: "R-Defense-MortarPit-Incenediary", stat: "Emplacement-MortarPit-Incenediary", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-Howitzer-Incenediary", stat: "Emplacement-Howitzer-Incenediary", defrole: DEFROLE.ARTY }, + ], + templates: [], + extras: [ + "R-Wpn-Bomb-Accuracy03", + "R-Wpn-Howitzer-Damage06", + "R-Wpn-Howitzer-ROF04", + "R-Wpn-Howitzer-Accuracy03", + ], + }, + rockets_AT: { + roles: [ 1.0, 0.0, 0.0, 0.0 ], + chatalias: "rx", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Rocket05-MiniPod", stat: "Rocket-Pod", weight: WEIGHT.LIGHT }, // pod + { res: "R-Wpn-Rocket01-LtAT", stat: "Rocket-LtA-T", weight: WEIGHT.LIGHT }, // lancer + { res: "R-Wpn-Rocket07-Tank-Killer", stat: "Rocket-HvyA-T", weight: WEIGHT.LIGHT }, // tk + { res: "R-Wpn-Missile2A-T", stat: "Missile-A-T", weight: WEIGHT.LIGHT }, // scourge + ], + vtols: [ + { res: "R-Wpn-Rocket05-MiniPod", stat: "Rocket-VTOL-Pod", weight: WEIGHT.ULTRALIGHT }, // pod + { res: "R-Wpn-Rocket01-LtAT", stat: "Rocket-VTOL-LtA-T", weight: WEIGHT.LIGHT }, // lancer + { res: "R-Wpn-Rocket07-Tank-Killer", stat: "Rocket-VTOL-HvyA-T", weight: WEIGHT.LIGHT }, // tk + { res: "R-Wpn-Missile2A-T", stat: "Missile-VTOL-AT", weight: WEIGHT.LIGHT }, // scourge + ], + defenses: [ + // rocket turtle AI needs early AT gateway towers, hence duplicate stat + { res: "R-Defense-Tower06", stat: "GuardTower6", defrole: DEFROLE.GATEWAY }, // pod tower + { res: "R-Defense-Tower06", stat: "GuardTower6", defrole: DEFROLE.STANDALONE }, // pod tower + { res: "R-Defense-Pillbox06", stat: "GuardTower5", defrole: DEFROLE.STANDALONE }, // lancer tower + { res: "R-Defense-WallTower06", stat: "WallTower06", defrole: DEFROLE.GATEWAY }, // lancer hardpoint + { res: "R-Defense-HvyA-Trocket", stat: "Emplacement-HvyATrocket", defrole: DEFROLE.STANDALONE }, // tk emplacement + { res: "R-Defense-WallTower-HvyA-Trocket", stat: "WallTower-HvATrocket", defrole: DEFROLE.GATEWAY }, // tk hardpoint + { res: "R-Defense-Super-Rocket", stat: "X-Super-Rocket", defrole: DEFROLE.FORTRESS }, // rocket bastion + { res: "R-Defense-GuardTower-ATMiss", stat: "GuardTower-ATMiss", defrole: DEFROLE.STANDALONE }, // scourge tower + { res: "R-Defense-WallTower-A-Tmiss", stat: "WallTower-Atmiss", defrole: DEFROLE.GATEWAY }, // scourge hardpoint + { res: "R-Defense-Super-Missile", stat: "X-Super-Missile", defrole: DEFROLE.FORTRESS }, // missile fortress + ], + templates: [ + { res: "R-Wpn-Rocket01-LtAT", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgRocket", ] }, // lancer borg + { res: "R-Cyborg-Hvywpn-TK", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-TK", ] }, // tk super + { res: "R-Wpn-Missile2A-T", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Atmiss", ] }, // scourge borg + { res: "R-Cyborg-Hvywpn-A-T", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-A-T", ] }, // scourge super + ], + extras: [ + "R-Wpn-Rocket-ROF03", + "R-Wpn-Missile-Damage03", + "R-Wpn-Missile-ROF03", + "R-Wpn-Missile-Accuracy02", + ], + }, + rockets_Arty: { + roles: [ 0.2, 0.4, 0.4, 0.0 ], + chatalias: "rxarty", + micro: MICRO.DUMB, + weapons: [ + { res: "R-Wpn-Rocket02-MRL", stat: "Rocket-MRL", weight: WEIGHT.LIGHT }, // mra + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-BB", weight: WEIGHT.MEDIUM }, // bb + { res: "R-Wpn-Rocket06-IDF", stat: "Rocket-IDF", weight: WEIGHT.ULTRAHEAVY }, // ripple + { res: "R-Wpn-MdArtMissile", stat: "Missile-MdArt", weight: WEIGHT.HEAVY }, // seraph + { res: "R-Wpn-HvArtMissile", stat: "Missile-HvyArt", weight: WEIGHT.ULTRAHEAVY }, // archie + ], + vtols: [ + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-VTOL-BB", weight: WEIGHT.LIGHT }, // bb + ], + defenses: [ + { res: "R-Defense-MRL", stat: "Emplacement-MRL-pit", defrole: DEFROLE.STANDALONE }, // mra + { res: "R-Defense-IDFRocket", stat: "Emplacement-Rocket06-IDF", defrole: DEFROLE.ARTY }, // ripple + { res: "R-Defense-MdArtMissile", stat: "Emplacement-MdART-pit", defrole: DEFROLE.STANDALONE }, // seraph + { res: "R-Defense-HvyArtMissile", stat: "Emplacement-HvART-pit", defrole: DEFROLE.ARTY }, // archie + ], + templates: [], + extras: [], + }, + rockets_AS: { + roles: [ 0.0, 0.0, 1.0, 0.0 ], + chatalias: "rxas", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-BB", weight: WEIGHT.MEDIUM }, // bb + ], + vtols: [ + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-VTOL-BB", weight: WEIGHT.LIGHT }, // bb + ], + defenses: [], + templates: [], + extras: [], + }, + rockets_AA: { + roles: [ 0.0, 0.0, 0.0, 1.0 ], + chatalias: "rxaa", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Sunburst", stat: "Rocket-Sunburst", weight: WEIGHT.LIGHT }, // sunburst + { res: "R-Wpn-Missile-LtSAM", stat: "Missile-LtSAM", weight: WEIGHT.LIGHT }, // avenger + { res: "R-Wpn-Missile-HvSAM", stat: "Missile-HvySAM", weight: WEIGHT.HEAVY }, // vindicator + ], + vtols: [ + { res: "R-Wpn-Sunburst", stat: "Rocket-VTOL-Sunburst", weight: WEIGHT.LIGHT }, // sunburst a2a + ], + defenses: [ + { res: "R-Defense-Sunburst", stat: "P0-AASite-Sunburst", defrole: DEFROLE.STANDALONE }, // sunburst + { res: "R-Defense-SamSite1", stat: "P0-AASite-SAM1", defrole: DEFROLE.STANDALONE }, // avenger + { res: "R-Defense-WallTower-SamSite", stat: "WallTower-SamSite", defrole: DEFROLE.GATEWAY }, // avenger + { res: "R-Defense-SamSite2", stat: "P0-AASite-SAM2", defrole: DEFROLE.STANDALONE }, // vindicator + { res: "R-Defense-WallTower-SamHvy", stat: "WallTower-SamHvy", defrole: DEFROLE.GATEWAY }, // vindicator hardpoint + ], + templates: [], + extras: [], + }, + lasers: { + roles: [ 0.2, 0.6, 0.2, 0.0], + chatalias: "ls", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Laser01", stat: "Laser3BEAMMk1", weight: WEIGHT.ULTRALIGHT }, // flash + { res: "R-Wpn-Laser02", stat: "Laser2PULSEMk1", weight: WEIGHT.HEAVY }, // pulse + { res: "R-Wpn-HvyLaser", stat: "HeavyLaser", weight: WEIGHT.ULTRAHEAVY }, // hvy laser + ], + vtols: [ + { res: "R-Wpn-Laser01", stat: "Laser3BEAM-VTOL", weight: WEIGHT.ULTRALIGHT }, // flash + { res: "R-Wpn-Laser02", stat: "Laser2PULSE-VTOL", weight: WEIGHT.HEAVY }, // pulse + { res: "R-Wpn-HvyLaser", stat: "HeavyLaser-VTOL", weight: WEIGHT.HEAVY }, // hvy laser + ], + defenses: [ + { res: "R-Defense-PrisLas", stat: "Emplacement-PrisLas", defrole: DEFROLE.STANDALONE }, // flash empl + { res: "R-Defense-PulseLas", stat: "GuardTower-BeamLas", defrole: DEFROLE.STANDALONE }, // pulse tower + { res: "R-Defense-WallTower-PulseLas", stat: "WallTower-PulseLas", defrole: DEFROLE.GATEWAY }, // pulse hard + { res: "R-Defense-HeavyLas", stat: "Emplacement-HeavyLaser", defrole: DEFROLE.STANDALONE }, // hvy empl + ], + templates: [ + { res: "R-Wpn-Laser01", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Laser", ] }, // flash borg + { res: "Cyb-Hvy-PulseLsr", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-PulseLsr", ] }, // pulse super + ], + extras: [], + }, + useless_AT: { + roles: [ 1.0, 0.0, 0.0, 0.0 ], + chatalias: "useless_AT", + weapons: [ + { stat: "CannonSuper" }, // cannon fort weapon + { stat: "RocketSuper" }, // rocket bastion weapon + { stat: "MissileSuper" }, // missile fort weapon + { stat: "MassDriver" }, // mass driver fort weapon + { stat: "MortarEMP" }, // emp mortar + { stat: "EMP-Cannon" }, // emp cannon + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, + useless_AP: { + roles: [ 0.0, 1.0, 0.0, 0.0 ], + chatalias: "useless_AP", + weapons: [ + { stat: "MG1-Pillbox" }, // imaginary invisible single mg, may be found on some maps + { stat: "MG2-Pillbox" }, // imaginary invisible twin mg, may be found on some maps + { stat: "MG3-Pillbox" }, // mg bunker dedicated weapon + { stat: "NEXUSlink" }, // nexus link (still unused) + { stat: "MG4ROTARY-Pillbox" }, // ag bunker dedicated weapon + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, + useless_AS: { + roles: [ 0.0, 0.0, 1.0, 0.0 ], + chatalias: "useless_AS", + weapons: [ + { stat: "PlasmaHeavy" }, // plasma cannon (still unused) + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, + useless_AA: { + roles: [ 0.0, 0.0, 0.0, 1.0 ], + chatalias: "useless_AA", + weapons: [ + { stat: "QuadMg1AAGun" }, // hurricane (still unused) + { stat: "QuadRotAAGun" }, // whirlwind (still unused) + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, +}; diff --git a/har_rulesets/standard.js b/har_rulesets/standard.js new file mode 100644 index 0000000..977acfa --- /dev/null +++ b/har_rulesets/standard.js @@ -0,0 +1,564 @@ + +/* + * This file describes standard stats and strategies of + * the base (unmodded) game. + * + * If you want to make an AI specially designed for your mod, start by + * making a copy of this file and modifying it according to your mod's rules. + * + * Then provide a personality to use the ruleset, similar to + * how har_generic.[js|ai] is provided for this ruleset. + * + * You may find some useful functions for working with these stats + * in stats.js . + * + */ + +// a factor for figuring out how large things are in this ruleset, +// or simply a typical radius of a player's base +const baseScale = 20; + +// diameter of laser satellite splash/incendiary damage +// for use in lassat.js +const lassatSplash = 4; + +// set this to 1 to choose templates randomly, instead of later=>better. +const randomTemplates = 0; + +// this function is used for avoiding AI cheats that appear due to +// being able to build droids before designing them +function iCanDesign() { + if (difficulty === INSANE) // won't make INSANE much worse ... + return true; + return countFinishedStructList(structures.hqs) > 0; +} + +const structures = { + factories: [ "A0LightFactory", ], + templateFactories: [ "A0CyborgFactory", ], + vtolFactories: [ "A0VTolFactory1", ], + labs: [ "A0ResearchFacility", ], + gens: [ "A0PowerGenerator", ], + hqs: [ "A0CommandCentre", ], + vtolPads: [ "A0VtolPad", ], + derricks: [ "A0ResourceExtractor", ], + extras: [ "A0RepairCentre3", "A0Sat-linkCentre", "A0LasSatCommand", ], + sensors: [ "Sys-SensoTower02", "Sys-CB-Tower01", "Sys-RadarDetector01", "Sys-SensoTowerWS", ], +}; + +const oilResources = [ "OilResource", ]; + +const powerUps = [ "OilDrum", "Crate" ]; + +// NOTE: you cannot use specific stats as bases, but only stattypes +// probably better make use of .name rather than of .stattype here? +const modules = [ + { base: POWER_GEN, module: "A0PowMod1", count: 1, cost: MODULECOST.CHEAP }, + { base: FACTORY, module: "A0FacMod1", count: 2, cost: MODULECOST.EXPENSIVE }, + { base: VTOL_FACTORY, module: "A0FacMod1", count: 2, cost: MODULECOST.EXPENSIVE }, + { base: RESEARCH_LAB, module: "A0ResearchModule1", count: 1, cost: MODULECOST.EXPENSIVE }, +]; + +const targets = [] + .concat(structures.factories) + .concat(structures.templateFactories) + .concat(structures.vtolFactories) + .concat(structures.extras) +; + +const miscTargets = [] + .concat(structures.derricks) +; + +const sensorTurrets = [ + "SensorTurret1Mk1", // sensor + "Sensor-WideSpec", // wide spectrum sensor +]; + +const fundamentalResearch = [ + "R-Struc-PowerModuleMk1", + "R-Struc-RprFac-Upgrade01", + "R-Sys-Sensor-Tower02", + "R-Vehicle-Prop-Halftracks", + "R-Struc-Power-Upgrade01c", + "R-Vehicle-Prop-Tracks", + "R-Sys-CBSensor-Tower01", + "R-Struc-VTOLPad-Upgrade01", + "R-Struc-Power-Upgrade03a", + "R-Struc-VTOLPad-Upgrade03", + "R-Sys-Autorepair-General", + "R-Wpn-LasSat", + "R-Struc-RprFac-Upgrade04", + "R-Struc-VTOLPad-Upgrade06", + "R-Struc-RprFac-Upgrade06", +]; + +const fastestResearch = [ + "R-Struc-Research-Upgrade09", +]; + +// body and propulsion arrays don't affect fixed template droids +const bodyStats = [ + { res: "R-Vehicle-Body01", stat: "Body1REC", weight: WEIGHT.LIGHT, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.KINETIC }, // viper + { res: "R-Vehicle-Body05", stat: "Body5REC", weight: WEIGHT.MEDIUM, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.KINETIC }, // cobra + { res: "R-Vehicle-Body11", stat: "Body11ABT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // python + { res: "R-Vehicle-Body02", stat: "Body2SUP", weight: WEIGHT.LIGHT, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.KINETIC }, // leopard + { res: "R-Vehicle-Body06", stat: "Body6SUPP", weight: WEIGHT.MEDIUM, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.KINETIC }, // panther + { res: "R-Vehicle-Body09", stat: "Body9REC", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // tiger + { res: "R-Vehicle-Body13", stat: "Body13SUP", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // wyvern + { res: "R-Vehicle-Body14", stat: "Body14SUP", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.KINETIC }, // dragon + { res: "R-Vehicle-Body04", stat: "Body4ABT", weight: WEIGHT.LIGHT, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.THERMAL }, // bug + { res: "R-Vehicle-Body08", stat: "Body8MBT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.THERMAL }, // scorpion + { res: "R-Vehicle-Body12", stat: "Body12SUP", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.THERMAL }, // mantis + { res: "R-Vehicle-Body03", stat: "Body3MBT", weight: WEIGHT.MEDIUM, usage: BODYUSAGE.UNIVERSAL, armor: BODYCLASS.THERMAL }, // retaliation + { res: "R-Vehicle-Body07", stat: "Body7ABT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.COMBAT, armor: BODYCLASS.THERMAL }, // retribution + { res: "R-Vehicle-Body10", stat: "Body10MBT", weight: WEIGHT.HEAVY, usage: BODYUSAGE.GROUND, armor: BODYCLASS.THERMAL }, // vengeance +]; + +const classResearch = { + kinetic: [ + [ // OBJTYPE.TANK + "R-Vehicle-Metals09", + ], + [ // OBJTYPE.BORG + "R-Cyborg-Metals09", + ], + [ // OBJTYPE.DEFS + "R-Defense-WallUpgrade03", + "R-Struc-Materials03", + "R-Defense-WallUpgrade06", + "R-Struc-Materials06", + "R-Defense-WallUpgrade12", + "R-Struc-Materials09", + ], + [ // OBJTYPE.VTOL + "R-Vehicle-Metals09", + ], + ], + thermal: [ + [ + "R-Vehicle-Armor-Heat09", + ], + [ + "R-Cyborg-Armor-Heat09", + ], + [ + "R-Defense-WallUpgrade03", + "R-Struc-Materials03", + "R-Defense-WallUpgrade06", + "R-Struc-Materials06", + "R-Defense-WallUpgrade12", + "R-Struc-Materials09", + ], + [ + "R-Vehicle-Armor-Heat09", + ], + ], +} + +// NOTE: Please don't put hover propulsion into the ground list, etc.! +// NOTE: Hover propulsion should be placed AFTER ground propulsion! +// Adaptation code relies on that for discovering map topology. +// Ground propulsions need to be ground only, hover propulsions shouldn't +// be able to cross cliffs, but should be able to cross seas, etc. +const propulsionStats = [ + { res: "R-Vehicle-Prop-Wheels", stat: "wheeled01", usage: PROPULSIONUSAGE.GROUND }, + { res: "R-Vehicle-Prop-Halftracks", stat: "HalfTrack", usage: PROPULSIONUSAGE.GROUND }, + { res: "R-Vehicle-Prop-Tracks", stat: "tracked01", usage: PROPULSIONUSAGE.GROUND }, + { res: "R-Vehicle-Prop-Hover", stat: "hover01", usage: PROPULSIONUSAGE.HOVER }, + { res: "R-Vehicle-Prop-VTOL", stat: "V-Tol", usage: PROPULSIONUSAGE.VTOL }, +]; + + +const truckTurrets = [ + "Spade1Mk1", +]; + +const truckTemplates = [ + { body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgSpade", ] } // engineer +]; + +const fallbackWeapon = 'machineguns'; + +// Unlike bodies and propulsions, weapon lines don't have any specific meaning. +// You can make as many weapon lines as you want for your ruleset. +const weaponStats = { + machineguns: { + // How good weapons of this path are against tanks, borgs, defenses, vtols? + // The sum of the four should be equal to 1. + roles: [ 0.1, 0.7, 0.1, 0.1 ], + // This explains how are human players supposed to call this weapon path in the chat. + chatalias: "mg", + // This controls micromanagement of units based on the weapons of this path. + micro: MICRO.RANGED, + // Weapons of the path, better weapons below. + weapons: [ + { res: "R-Wpn-MG1Mk1", stat: "MG1Mk1", weight: WEIGHT.ULTRALIGHT }, // mg + { res: "R-Wpn-MG2Mk1", stat: "MG2Mk1", weight: WEIGHT.LIGHT }, // tmg + { res: "R-Wpn-MG3Mk1", stat: "MG3Mk1", weight: WEIGHT.MEDIUM }, // hmg + { res: "R-Wpn-MG4", stat: "MG4ROTARYMk1", weight: WEIGHT.MEDIUM }, // ag + { res: "R-Wpn-MG5", stat: "MG5TWINROTARY", weight: WEIGHT.MEDIUM }, // tag + ], + // VTOL weapons of the path, in the same order. + vtols: [ + { res: "R-Wpn-MG3Mk1", stat: "MG3-VTOL", weight: WEIGHT.ULTRALIGHT }, // vtol hmg + { res: "R-Wpn-MG4", stat: "MG4ROTARY-VTOL", weight: WEIGHT.MEDIUM }, // vtol ag + ], + // Defensive structures of the path, in the same order. + // NOTE: a defensive structure is recycled whenever there are at least two structures + // with the same role available down the list + defenses: [ + // turtle AI needs early versatile towers, hence duplicate stat + { res: "R-Defense-Tower01", stat: "GuardTower1", defrole: DEFROLE.GATEWAY }, // hmg tower + { res: "R-Defense-Tower01", stat: "GuardTower1", defrole: DEFROLE.STANDALONE }, // hmg tower + { res: "R-Defense-Pillbox01", stat: "PillBox1", defrole: DEFROLE.STANDALONE }, // hmg bunker + { res: "R-Defense-WallTower01", stat: "WallTower01", defrole: DEFROLE.GATEWAY }, // hmg hardpoint + { res: "R-Defense-RotMG", stat: "Pillbox-RotMG", defrole: DEFROLE.STANDALONE }, // ag bunker + { res: "R-Defense-Wall-RotMg", stat: "Wall-RotMg", defrole: DEFROLE.GATEWAY }, // ag hardpoint + { res: "R-Defense-WallTower-TwinAGun", stat: "WallTower-TwinAssaultGun", defrole: DEFROLE.GATEWAY }, // tag hardpoint + ], + // Cyborg templates, better borgs below, as usual. + templates: [ + { res: "R-Wpn-MG1Mk1", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgChaingun", ] }, // mg cyborg + { res: "R-Wpn-MG4", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgRotMG", ] }, // ag cyborg + ], + // Extra things to research on this path, even if they don't lead to any new stuff + extras: [ + "R-Wpn-MG-Damage08", + ], + }, + flamers: { + roles: [ 0.3, 0.7, 0.0, 0.0 ], + chatalias: "fl", + micro: MICRO.MELEE, + weapons: [ + { res: "R-Wpn-Flamer01Mk1", stat: "Flame1Mk1", weight: WEIGHT.LIGHT }, // flamer + { res: "R-Wpn-Flame2", stat: "Flame2", weight: WEIGHT.HEAVY }, // inferno + { res: "R-Wpn-Plasmite-Flamer", stat: "PlasmiteFlamer", weight: WEIGHT.HEAVY }, // plasmite + ], + vtols: [], + defenses: [ + { res: "R-Defense-Pillbox05", stat: "PillBox5", defrole: DEFROLE.GATEWAY }, // flamer bunker + { res: "R-Defense-HvyFlamer", stat: "Tower-Projector", defrole: DEFROLE.GATEWAY }, // inferno bunker + { res: "R-Defense-PlasmiteFlamer", stat: "Plasmite-flamer-bunker", defrole: DEFROLE.GATEWAY }, // plasmite bunker + ], + templates: [ + { res: "R-Wpn-Flamer01Mk1", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgFlamer01", ] }, // flamer cyborg + { res: "R-Wpn-Flame2", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Thermite", ] }, // flamer cyborg + ], + extras: [ + "R-Wpn-Flamer-ROF03", + "R-Wpn-Flamer-Damage09", + ], + }, + cannons: { + roles: [ 0.5, 0.2, 0.3, 0.0 ], + chatalias: "cn", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Cannon1Mk1", stat: "Cannon1Mk1", weight: WEIGHT.LIGHT }, // lc + { res: "R-Wpn-Cannon2Mk1", stat: "Cannon2A-TMk1", weight: WEIGHT.HEAVY }, // mc + { res: "R-Wpn-Cannon4AMk1", stat: "Cannon4AUTOMk1", weight: WEIGHT.HEAVY }, // hpv + { res: "R-Wpn-Cannon5", stat: "Cannon5VulcanMk1", weight: WEIGHT.HEAVY }, // ac + { res: "R-Wpn-Cannon6TwinAslt", stat: "Cannon6TwinAslt", weight: WEIGHT.ULTRAHEAVY }, // tac + { res: "R-Wpn-Cannon3Mk1", stat: "Cannon375mmMk1", weight: WEIGHT.ULTRAHEAVY }, // hc + { res: "R-Wpn-RailGun01", stat: "RailGun1Mk1", weight: WEIGHT.LIGHT }, // needle + { res: "R-Wpn-RailGun02", stat: "RailGun2Mk1", weight: WEIGHT.HEAVY }, // rail + { res: "R-Wpn-RailGun03", stat: "RailGun3Mk1", weight: WEIGHT.ULTRAHEAVY }, // gauss + ], + vtols: [ + { res: "R-Wpn-Cannon1Mk1", stat: "Cannon1-VTOL", weight: WEIGHT.LIGHT }, // lc + { res: "R-Wpn-Cannon4AMk1", stat: "Cannon4AUTO-VTOL", weight: WEIGHT.HEAVY }, // hpv + { res: "R-Wpn-Cannon5", stat: "Cannon5Vulcan-VTOL", weight: WEIGHT.HEAVY }, // ac + { res: "R-Wpn-RailGun01", stat: "RailGun1-VTOL", weight: WEIGHT.LIGHT }, // needle + { res: "R-Wpn-RailGun02", stat: "RailGun2-VTOL", weight: WEIGHT.HEAVY }, // rail + ], + defenses: [ + { res: "R-Defense-Pillbox04", stat: "PillBox4", defrole: DEFROLE.STANDALONE }, // lc bunker + { res: "R-Defense-WallTower02", stat: "WallTower02", defrole: DEFROLE.GATEWAY }, // lc hard + { res: "R-Defense-WallTower03", stat: "WallTower03", defrole: DEFROLE.GATEWAY }, // mc hard + { res: "R-Defense-Emplacement-HPVcannon", stat: "Emplacement-HPVcannon", defrole: DEFROLE.STANDALONE }, // hpv empl + { res: "R-Defense-WallTower-HPVcannon", stat: "WallTower-HPVcannon", defrole: DEFROLE.GATEWAY }, // hpv hard + { res: "R-Defense-Wall-VulcanCan", stat: "Wall-VulcanCan", defrole: DEFROLE.GATEWAY }, // ac hard + { res: "R-Defense-Cannon6", stat: "PillBox-Cannon6", defrole: DEFROLE.STANDALONE }, // tac bunker + { res: "R-Defense-WallTower04", stat: "WallTower04", defrole: DEFROLE.GATEWAY }, // hc hard + { res: "R-Defense-Super-Cannon", stat: "X-Super-Cannon", defrole: DEFROLE.FORTRESS }, // cannon fort + { res: "R-Defense-GuardTower-Rail1", stat: "GuardTower-Rail1", defrole: DEFROLE.STANDALONE }, // needle tower + { res: "R-Defense-Rail2", stat: "Emplacement-Rail2", defrole: DEFROLE.STANDALONE }, // rail empl + { res: "R-Defense-WallTower-Rail2", stat: "WallTower-Rail2", defrole: DEFROLE.GATEWAY }, // rail hard + { res: "R-Defense-Rail3", stat: "Emplacement-Rail3", defrole: DEFROLE.STANDALONE }, // gauss empl + { res: "R-Defense-WallTower-Rail3", stat: "WallTower-Rail3", defrole: DEFROLE.GATEWAY }, // gauss hard + { res: "R-Defense-MassDriver", stat: "X-Super-MassDriver", defrole: DEFROLE.FORTRESS }, // mass driver fort + ], + templates: [ + { res: "R-Wpn-Cannon1Mk1", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgCannon", ] }, // lc borg + { res: "R-Cyborg-Hvywpn-Mcannon", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-Mcannon", ] }, // mc super + { res: "R-Cyborg-Hvywpn-HPV", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-HPV", ] }, // hpv super + { res: "R-Cyborg-Hvywpn-Acannon", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-Acannon", ] }, // ac super + { res: "R-Wpn-RailGun01", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Rail1", ] }, // needle borg + { res: "R-Cyborg-Hvywpn-RailGunner", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-RailGunner", ] }, // rail super + ], + extras: [ + "R-Wpn-Cannon-ROF06", + "R-Vehicle-Engine09", // cannons are heeeeavy + "R-Wpn-Rail-Damage03", // sure it's required by gauss, but what if our AI uses only cyborgs and vtols? + "R-Wpn-Rail-ROF03", + ], + }, + cannons_AA: { + roles: [ 0.0, 0.0, 0.0, 1.0 ], + chatalias: "ca", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-AAGun02", stat: "AAGun2Mk1", weight: WEIGHT.HEAVY }, + ], + vtols: [], + defenses: [ + { res: "R-Defense-AASite-QuadBof", stat: "AASite-QuadBof", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-WallTower-DoubleAAgun", stat: "WallTower-DoubleAAGun", defrole: DEFROLE.GATEWAY }, + ], + templates: [], + extras: [], + }, + mortars: { + roles: [ 0.2, 0.4, 0.4, 0.0 ], + chatalias: "mo", + micro: MICRO.DUMB, + weapons: [ + { res: "R-Wpn-Mortar01Lt", stat: "Mortar1Mk1", weight: WEIGHT.HEAVY }, // duplicate stat! + { res: "R-Wpn-Mortar02Hvy", stat: "Mortar2Mk1", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-Mortar3", stat: "Mortar3ROTARYMk1", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-HowitzerMk1", stat: "Howitzer105Mk1", weight: WEIGHT.ULTRAHEAVY }, + { res: "R-Wpn-Howitzer03-Rot", stat: "Howitzer03-Rot", weight: WEIGHT.ULTRAHEAVY }, + { res: "R-Wpn-HvyHowitzer", stat: "Howitzer150Mk1", weight: WEIGHT.ULTRAHEAVY }, + ], + vtols: [ + { res: "R-Wpn-Bomb01", stat: "Bomb1-VTOL-LtHE", weight: WEIGHT.LIGHT }, + { res: "R-Wpn-Bomb02", stat: "Bomb2-VTOL-HvHE", weight: WEIGHT.HEAVY }, + ], + defenses: [ + { res: "R-Defense-MortarPit", stat: "Emplacement-MortarPit01", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-HvyMor", stat: "Emplacement-MortarPit02", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-RotMor", stat: "Emplacement-RotMor", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-Howitzer", stat: "Emplacement-Howitzer105", defrole: DEFROLE.ARTY }, + { res: "R-Defense-RotHow", stat: "Emplacement-RotHow", defrole: DEFROLE.ARTY }, + { res: "R-Defense-HvyHowitzer", stat: "Emplacement-Howitzer150", defrole: DEFROLE.ARTY }, + ], + templates: [ + { res: "R-Wpn-Mortar01Lt", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Grenade", ] }, + ], + extras: [ + "R-Wpn-Bomb-Accuracy03", + "R-Wpn-Howitzer-Damage06", + "R-Wpn-Howitzer-ROF04", + "R-Wpn-Howitzer-Accuracy03", + ], + }, + fireMortars: { + roles: [ 0.3, 0.3, 0.4, 0.0 ], + chatalias: "fm", + micro: MICRO.DUMB, + weapons: [ + { res: "R-Wpn-Mortar01Lt", stat: "Mortar1Mk1", weight: WEIGHT.HEAVY }, // duplicate stat! + { res: "R-Wpn-Mortar-Incenediary", stat: "Mortar-Incenediary", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-Howitzer-Incenediary", stat: "Howitzer-Incenediary", weight: WEIGHT.ULTRAHEAVY }, + ], + vtols: [ + { res: "R-Wpn-Bomb03", stat: "Bomb3-VTOL-LtINC", weight: WEIGHT.LIGHT }, + { res: "R-Wpn-Bomb04", stat: "Bomb4-VTOL-HvyINC", weight: WEIGHT.HEAVY }, + { res: "R-Wpn-Bomb05", stat: "Bomb5-VTOL-Plasmite", weight: WEIGHT.HEAVY }, + ], + defenses: [ + { res: "R-Defense-MortarPit-Incenediary", stat: "Emplacement-MortarPit-Incenediary", defrole: DEFROLE.STANDALONE }, + { res: "R-Defense-Howitzer-Incenediary", stat: "Emplacement-Howitzer-Incenediary", defrole: DEFROLE.ARTY }, + ], + templates: [], + extras: [ + "R-Wpn-Bomb-Accuracy03", + "R-Wpn-Howitzer-Damage06", + "R-Wpn-Howitzer-ROF04", + "R-Wpn-Howitzer-Accuracy03", + ], + }, + rockets_AT: { + roles: [ 1.0, 0.0, 0.0, 0.0 ], + chatalias: "rx", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Rocket05-MiniPod", stat: "Rocket-Pod", weight: WEIGHT.LIGHT }, // pod + { res: "R-Wpn-Rocket01-LtAT", stat: "Rocket-LtA-T", weight: WEIGHT.LIGHT }, // lancer + { res: "R-Wpn-Rocket07-Tank-Killer", stat: "Rocket-HvyA-T", weight: WEIGHT.LIGHT }, // tk + { res: "R-Wpn-Missile2A-T", stat: "Missile-A-T", weight: WEIGHT.LIGHT }, // scourge + ], + vtols: [ + { res: "R-Wpn-Rocket05-MiniPod", stat: "Rocket-VTOL-Pod", weight: WEIGHT.ULTRALIGHT }, // pod + { res: "R-Wpn-Rocket01-LtAT", stat: "Rocket-VTOL-LtA-T", weight: WEIGHT.LIGHT }, // lancer + { res: "R-Wpn-Rocket07-Tank-Killer", stat: "Rocket-VTOL-HvyA-T", weight: WEIGHT.LIGHT }, // tk + { res: "R-Wpn-Missile2A-T", stat: "Missile-VTOL-AT", weight: WEIGHT.LIGHT }, // scourge + ], + defenses: [ + // rocket turtle AI needs early AT gateway towers, hence duplicate stat + { res: "R-Defense-Tower06", stat: "GuardTower6", defrole: DEFROLE.GATEWAY }, // pod tower + { res: "R-Defense-Tower06", stat: "GuardTower6", defrole: DEFROLE.STANDALONE }, // pod tower + { res: "R-Defense-Pillbox06", stat: "GuardTower5", defrole: DEFROLE.STANDALONE }, // lancer tower + { res: "R-Defense-WallTower06", stat: "WallTower06", defrole: DEFROLE.GATEWAY }, // lancer hardpoint + { res: "R-Defense-HvyA-Trocket", stat: "Emplacement-HvyATrocket", defrole: DEFROLE.STANDALONE }, // tk emplacement + { res: "R-Defense-WallTower-HvyA-Trocket", stat: "WallTower-HvATrocket", defrole: DEFROLE.GATEWAY }, // tk hardpoint + { res: "R-Defense-Super-Rocket", stat: "X-Super-Rocket", defrole: DEFROLE.FORTRESS }, // rocket bastion + { res: "R-Defense-GuardTower-ATMiss", stat: "GuardTower-ATMiss", defrole: DEFROLE.STANDALONE }, // scourge tower + { res: "R-Defense-WallTower-A-Tmiss", stat: "WallTower-Atmiss", defrole: DEFROLE.GATEWAY }, // scourge hardpoint + { res: "R-Defense-Super-Missile", stat: "X-Super-Missile", defrole: DEFROLE.FORTRESS }, // missile fortress + ], + templates: [ + { res: "R-Wpn-Rocket01-LtAT", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "CyborgRocket", ] }, // lancer borg + { res: "R-Cyborg-Hvywpn-TK", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-TK", ] }, // tk super + { res: "R-Wpn-Missile2A-T", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Atmiss", ] }, // scourge borg + { res: "R-Cyborg-Hvywpn-A-T", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-A-T", ] }, // scourge super + ], + extras: [ + "R-Wpn-Rocket-ROF03", + "R-Wpn-Missile-Damage03", + "R-Wpn-Missile-ROF03", + "R-Wpn-Missile-Accuracy02", + ], + }, + rockets_Arty: { + roles: [ 0.2, 0.4, 0.4, 0.0 ], + chatalias: "rxarty", + micro: MICRO.DUMB, + weapons: [ + { res: "R-Wpn-Rocket02-MRL", stat: "Rocket-MRL", weight: WEIGHT.LIGHT }, // mra + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-BB", weight: WEIGHT.MEDIUM }, // bb + { res: "R-Wpn-Rocket06-IDF", stat: "Rocket-IDF", weight: WEIGHT.ULTRAHEAVY }, // ripple + { res: "R-Wpn-MdArtMissile", stat: "Missile-MdArt", weight: WEIGHT.HEAVY }, // seraph + { res: "R-Wpn-HvArtMissile", stat: "Missile-HvyArt", weight: WEIGHT.ULTRAHEAVY }, // archie + ], + vtols: [ + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-VTOL-BB", weight: WEIGHT.LIGHT }, // bb + ], + defenses: [ + { res: "R-Defense-MRL", stat: "Emplacement-MRL-pit", defrole: DEFROLE.STANDALONE }, // mra + { res: "R-Defense-IDFRocket", stat: "Emplacement-Rocket06-IDF", defrole: DEFROLE.ARTY }, // ripple + { res: "R-Defense-MdArtMissile", stat: "Emplacement-MdART-pit", defrole: DEFROLE.STANDALONE }, // seraph + { res: "R-Defense-HvyArtMissile", stat: "Emplacement-HvART-pit", defrole: DEFROLE.ARTY }, // archie + ], + templates: [], + extras: [], + }, + rockets_AS: { + roles: [ 0.0, 0.0, 1.0, 0.0 ], + chatalias: "rxas", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-BB", weight: WEIGHT.MEDIUM }, // bb + ], + vtols: [ + { res: "R-Wpn-Rocket03-HvAT", stat: "Rocket-VTOL-BB", weight: WEIGHT.LIGHT }, // bb + ], + defenses: [], + templates: [], + extras: [], + }, + rockets_AA: { + roles: [ 0.0, 0.0, 0.0, 1.0 ], + chatalias: "rxaa", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Sunburst", stat: "Rocket-Sunburst", weight: WEIGHT.LIGHT }, // sunburst + { res: "R-Wpn-Missile-LtSAM", stat: "Missile-LtSAM", weight: WEIGHT.LIGHT }, // avenger + { res: "R-Wpn-Missile-HvSAM", stat: "Missile-HvySAM", weight: WEIGHT.HEAVY }, // vindicator + ], + vtols: [ + { res: "R-Wpn-Sunburst", stat: "Rocket-VTOL-Sunburst", weight: WEIGHT.LIGHT }, // sunburst a2a + ], + defenses: [ + { res: "R-Defense-Sunburst", stat: "P0-AASite-Sunburst", defrole: DEFROLE.STANDALONE }, // sunburst + { res: "R-Defense-SamSite1", stat: "P0-AASite-SAM1", defrole: DEFROLE.STANDALONE }, // avenger + { res: "R-Defense-WallTower-SamSite", stat: "WallTower-SamSite", defrole: DEFROLE.GATEWAY }, // avenger + { res: "R-Defense-SamSite2", stat: "P0-AASite-SAM2", defrole: DEFROLE.STANDALONE }, // vindicator + { res: "R-Defense-WallTower-SamHvy", stat: "WallTower-SamHvy", defrole: DEFROLE.GATEWAY }, // vindicator hardpoint + ], + templates: [], + extras: [], + }, + lasers: { + roles: [ 0.2, 0.6, 0.2, 0.0], + chatalias: "ls", + micro: MICRO.RANGED, + weapons: [ + { res: "R-Wpn-Laser01", stat: "Laser3BEAMMk1", weight: WEIGHT.ULTRALIGHT }, // flash + { res: "R-Wpn-Laser02", stat: "Laser2PULSEMk1", weight: WEIGHT.HEAVY }, // pulse + { res: "R-Wpn-HvyLaser", stat: "HeavyLaser", weight: WEIGHT.ULTRAHEAVY }, // hvy laser + ], + vtols: [ + { res: "R-Wpn-Laser01", stat: "Laser3BEAM-VTOL", weight: WEIGHT.ULTRALIGHT }, // flash + { res: "R-Wpn-Laser02", stat: "Laser2PULSE-VTOL", weight: WEIGHT.HEAVY }, // pulse + { res: "R-Wpn-HvyLaser", stat: "HeavyLaser-VTOL", weight: WEIGHT.HEAVY }, // hvy laser + ], + defenses: [ + { res: "R-Defense-PrisLas", stat: "Emplacement-PrisLas", defrole: DEFROLE.STANDALONE }, // flash empl + { res: "R-Defense-PulseLas", stat: "GuardTower-BeamLas", defrole: DEFROLE.STANDALONE }, // pulse tower + { res: "R-Defense-WallTower-PulseLas", stat: "WallTower-PulseLas", defrole: DEFROLE.GATEWAY }, // pulse hard + { res: "R-Defense-HeavyLas", stat: "Emplacement-HeavyLaser", defrole: DEFROLE.STANDALONE }, // hvy empl + ], + templates: [ + { res: "R-Wpn-Laser01", body: "CyborgLightBody", prop: "CyborgLegs", weapons: [ "Cyb-Wpn-Laser", ] }, // flash borg + { res: "Cyb-Hvy-PulseLsr", body: "CyborgHeavyBody", prop: "CyborgLegs", weapons: [ "Cyb-Hvywpn-PulseLsr", ] }, // pulse super + ], + extras: [], + }, + useless_AT: { + roles: [ 1.0, 0.0, 0.0, 0.0 ], + chatalias: "useless_AT", + weapons: [ + { stat: "CannonSuper" }, // cannon fort weapon + { stat: "RocketSuper" }, // rocket bastion weapon + { stat: "MissileSuper" }, // missile fort weapon + { stat: "MassDriver" }, // mass driver fort weapon + { stat: "MortarEMP" }, // emp mortar + { stat: "EMP-Cannon" }, // emp cannon + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, + useless_AP: { + roles: [ 0.0, 1.0, 0.0, 0.0 ], + chatalias: "useless_AP", + weapons: [ + { stat: "MG1-Pillbox" }, // imaginary invisible single mg, may be found on some maps + { stat: "MG2-Pillbox" }, // imaginary invisible twin mg, may be found on some maps + { stat: "MG3-Pillbox" }, // mg bunker dedicated weapon + { stat: "NEXUSlink" }, // nexus link (still unused) + { stat: "MG4ROTARY-Pillbox" }, // ag bunker dedicated weapon + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, + useless_AS: { + roles: [ 0.0, 0.0, 1.0, 0.0 ], + chatalias: "useless_AS", + weapons: [ + { stat: "PlasmaHeavy" }, // plasma cannon (still unused) + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, + useless_AA: { + roles: [ 0.0, 0.0, 0.0, 1.0 ], + chatalias: "useless_AA", + weapons: [ + { stat: "QuadMg1AAGun" }, // hurricane (still unused) + { stat: "QuadRotAAGun" }, // whirlwind (still unused) + ], + vtols: [], + defenses: [], + templates: [], + extras: [], + }, +}; diff --git a/harold_custom.js b/harold_custom.js new file mode 100644 index 0000000..9a3d8fa --- /dev/null +++ b/harold_custom.js @@ -0,0 +1,171 @@ + +/* + * This file defines a standard AI personality for the base game. + * + * It relies on ruleset definition in /rulesets/ to provide + * standard strategy descriptions and necessary game stat information. + * + * Then it passes control to the main code. + * + */ + +// You can redefine these paths when you make a customized AI +// for a map or a challenge. +HAR_PATH = "/multiplay/skirmish/"; +HAR_INCLUDES = HAR_PATH + "har_includes-custom/"; +HAR_RULESETS = HAR_PATH + "har_rulesets/"; + +// please don't touch this line +include(HAR_INCLUDES + "_head.js"); + +//////////////////////////////////////////////////////////////////////////////////////////// +// Start the actual personality definition + +// the rules in which this personality plays +include(HAR_RULESETS + "custom.js"); + +// variables defining the personality +var subpersonalities = { + MR: { + chatalias: "mr", + weaponPaths: [ // weapons to use; put late-game paths below! + weaponStats.rockets_AT, + weaponStats.machineguns, + weaponStats.rockets_AS, + weaponStats.rockets_AA, + weaponStats.rockets_Arty, + ], + earlyResearch: [ // fixed research path for the early game + "R-Wpn-MG-Damage01", + "R-Defense-Tower01", + "R-Vehicle-Prop-Halftracks", + "R-Struc-PowerModuleMk1", + "R-Wpn-MG-Damage03", + ], + minTanks: 1, // minimal attack force at game start + becomeHarder: 3, // how much to increase attack force every 5 minutes + maxTanks: 16, // maximum for the minTanks value (since it grows at becomeHarder rate) + minTrucks: 3, // minimal number of trucks around + minHoverTrucks: 4, // minimal number of hover trucks around + maxSensors: 1, // number of mobile sensor cars to produce + minMiscTanks: 1, // number of tanks to start harassing enemy + maxMiscTanks: 2, // number of tanks used for defense and harass + vtolness: 65, // the chance % of not making droids when adaptation mechanism chooses vtols + defensiveness: 65, // same thing for defenses; set this to 100 to enable turtle AI specific code + maxPower: 700, // build expensive things if we have more than that + repairAt: 50, // how much % healthy should droid be to join the attack group instead of repairing + }, + MC: { + chatalias: "mc", + weaponPaths: [ + weaponStats.cannons, + weaponStats.machineguns, + weaponStats.mortars, + weaponStats.cannons_AA, + ], + earlyResearch: [ + "R-Wpn-MG-Damage01", + "R-Vehicle-Prop-Halftracks", + "R-Wpn-Cannon1Mk1", + "R-Struc-PowerModuleMk1", + "R-Wpn-MG-Damage03", + ], + minTanks: 1, becomeHarder: 3, maxTanks: 16, + minTrucks: 3, minHoverTrucks: 4, maxSensors: 1, + minMiscTanks: 1, maxMiscTanks: 2, + vtolness: 65, defensiveness: 65, + maxPower: 700, + repairAt: 50, + }, + FR: { + chatalias: "fr", + weaponPaths: [ + weaponStats.rockets_AT, + weaponStats.flamers, + weaponStats.fireMortars, + weaponStats.rockets_AA, + weaponStats.lasers, + ], + earlyResearch: [ + "R-Wpn-Flamer-ROF01", + "R-Vehicle-Prop-Halftracks", + "R-Struc-PowerModuleMk1", + ], + minTanks: 1, becomeHarder: 3, maxTanks: 16, + minTrucks: 3, minHoverTrucks: 4, maxSensors: 1, + minMiscTanks: 1, maxMiscTanks: 2, + vtolness: 65, defensiveness: 65, + maxPower: 700, + repairAt: 50, + }, + FC: { + chatalias: "fc", + weaponPaths: [ + weaponStats.cannons, + weaponStats.flamers, + weaponStats.fireMortars, + weaponStats.cannons_AA, + weaponStats.lasers, + ], + earlyResearch: [ + "R-Wpn-Flamer-ROF01", + "R-Vehicle-Prop-Halftracks", + "R-Wpn-Cannon1Mk1", + "R-Struc-PowerModuleMk1", + ], + minTanks: 1, becomeHarder: 3, maxTanks: 16, + minTrucks: 3, minHoverTrucks: 4, maxSensors: 1, + minMiscTanks: 1, maxMiscTanks: 2, + vtolness: 65, defensiveness: 65, + maxPower: 700, + repairAt: 50, + }, +}; + +// this function describes the early build order +// you can rely on personality.chatalias for choosing different build orders for +// different subpersonalities +function buildOrder() { + var derrickCount = countFinishedStructList(structures.derricks); + // might be good for Insane AI, or for rebuilding + if (derrickCount > 0) + if (buildMinimum(structures.gens, 1)) return true; + // lab, factory, gen, cc - the current trivial build order for the 3.2+ starting conditions + if (buildMinimum(structures.labs, 1)) return true; + if (buildMinimum(structures.factories, 1)) return true; + if (buildMinimum(structures.gens, 1)) return true; + // make sure trucks go capture some oil at this moment + if (buildMinimumDerricks(1)) return true; + // what if one of them is being upgraded? will need the other anyway. + // also, it looks like the right timing in most cases. + if (buildMinimum(structures.gens, 2)) return true; + if (buildMinimum(structures.hqs, 1)) return true; + // make sure we have at least that much oils by now + if (buildMinimumDerricks(5)) return true; + // support hover maps + var ret = scopeRatings(); + if (ret.land === 0 && !iHaveHover()) + if (buildMinimum(structures.labs, 4)) return true; + if (ret.land === 0 && ret.sea === 0 && !iHaveVtol()) + if (buildMinimum(structures.labs, 4)) return true; + // since we reached that far, we can afford some more trucks + if (gameTime > 300000) { + personality.minTrucks = 5; + personality.maxPower = 300; + personality.minMiscTanks = 2; + personality.minMiscTanks = 6; + // build more factories and labs when we have enough income + if (buildMinimum(structures.labs, derrickCount / 3)) return true; + if (buildMinimum(structures.factories, 2)) return true; + if (buildMinimum(structures.templateFactories, 1)) return true; + if (buildMinimum(structures.vtolFactories, 1)) return true; + return false; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +// Proceed with the main code + +include(HAR_INCLUDES + "_main.js"); diff --git a/harold_custom.json b/harold_custom.json new file mode 100644 index 0000000..17ecb0a --- /dev/null +++ b/harold_custom.json @@ -0,0 +1,7 @@ +{ + "AI": { + "js": "harold_custom.js", + "name": "Harold-Custom", + "tip": "Private Version of Harold for test purposes" + } +} diff --git a/harold_generic.js b/harold_generic.js new file mode 100644 index 0000000..33c85f4 --- /dev/null +++ b/harold_generic.js @@ -0,0 +1,171 @@ + +/* + * This file defines a standard AI personality for the base game. + * + * It relies on ruleset definition in /rulesets/ to provide + * standard strategy descriptions and necessary game stat information. + * + * Then it passes control to the main code. + * + */ + +// You can redefine these paths when you make a customized AI +// for a map or a challenge. +HAR_PATH = "/multiplay/skirmish/"; +HAR_INCLUDES = HAR_PATH + "har_includes/"; +HAR_RULESETS = HAR_PATH + "har_rulesets/"; + +// please don't touch this line +include(HAR_INCLUDES + "_head.js"); + +//////////////////////////////////////////////////////////////////////////////////////////// +// Start the actual personality definition + +// the rules in which this personality plays +include(HAR_RULESETS + "standard.js"); + +// variables defining the personality +var subpersonalities = { + MR: { + chatalias: "mr", + weaponPaths: [ // weapons to use; put late-game paths below! + weaponStats.rockets_AT, + weaponStats.machineguns, + weaponStats.rockets_AS, + weaponStats.rockets_AA, + weaponStats.rockets_Arty, + ], + earlyResearch: [ // fixed research path for the early game + "R-Wpn-MG-Damage01", + "R-Defense-Tower01", + "R-Vehicle-Prop-Halftracks", + "R-Struc-PowerModuleMk1", + "R-Wpn-MG-Damage03", + ], + minTanks: 1, // minimal attack force at game start + becomeHarder: 3, // how much to increase attack force every 5 minutes + maxTanks: 16, // maximum for the minTanks value (since it grows at becomeHarder rate) + minTrucks: 3, // minimal number of trucks around + minHoverTrucks: 4, // minimal number of hover trucks around + maxSensors: 1, // number of mobile sensor cars to produce + minMiscTanks: 1, // number of tanks to start harassing enemy + maxMiscTanks: 2, // number of tanks used for defense and harass + vtolness: 65, // the chance % of not making droids when adaptation mechanism chooses vtols + defensiveness: 65, // same thing for defenses; set this to 100 to enable turtle AI specific code + maxPower: 700, // build expensive things if we have more than that + repairAt: 50, // how much % healthy should droid be to join the attack group instead of repairing + }, + MC: { + chatalias: "mc", + weaponPaths: [ + weaponStats.cannons, + weaponStats.machineguns, + weaponStats.mortars, + weaponStats.cannons_AA, + ], + earlyResearch: [ + "R-Wpn-MG-Damage01", + "R-Vehicle-Prop-Halftracks", + "R-Wpn-Cannon1Mk1", + "R-Struc-PowerModuleMk1", + "R-Wpn-MG-Damage03", + ], + minTanks: 1, becomeHarder: 3, maxTanks: 16, + minTrucks: 3, minHoverTrucks: 4, maxSensors: 1, + minMiscTanks: 1, maxMiscTanks: 2, + vtolness: 65, defensiveness: 65, + maxPower: 700, + repairAt: 50, + }, + FR: { + chatalias: "fr", + weaponPaths: [ + weaponStats.rockets_AT, + weaponStats.flamers, + weaponStats.fireMortars, + weaponStats.rockets_AA, + weaponStats.lasers, + ], + earlyResearch: [ + "R-Wpn-Flamer-ROF01", + "R-Vehicle-Prop-Halftracks", + "R-Struc-PowerModuleMk1", + ], + minTanks: 1, becomeHarder: 3, maxTanks: 16, + minTrucks: 3, minHoverTrucks: 4, maxSensors: 1, + minMiscTanks: 1, maxMiscTanks: 2, + vtolness: 65, defensiveness: 65, + maxPower: 700, + repairAt: 50, + }, + FC: { + chatalias: "fc", + weaponPaths: [ + weaponStats.cannons, + weaponStats.flamers, + weaponStats.fireMortars, + weaponStats.cannons_AA, + weaponStats.lasers, + ], + earlyResearch: [ + "R-Wpn-Flamer-ROF01", + "R-Vehicle-Prop-Halftracks", + "R-Wpn-Cannon1Mk1", + "R-Struc-PowerModuleMk1", + ], + minTanks: 1, becomeHarder: 3, maxTanks: 16, + minTrucks: 3, minHoverTrucks: 4, maxSensors: 1, + minMiscTanks: 1, maxMiscTanks: 2, + vtolness: 65, defensiveness: 65, + maxPower: 700, + repairAt: 50, + }, +}; + +// this function describes the early build order +// you can rely on personality.chatalias for choosing different build orders for +// different subpersonalities +function buildOrder() { + var derrickCount = countFinishedStructList(structures.derricks); + // might be good for Insane AI, or for rebuilding + if (derrickCount > 0) + if (buildMinimum(structures.gens, 1)) return true; + // lab, factory, gen, cc - the current trivial build order for the 3.2+ starting conditions + if (buildMinimum(structures.labs, 1)) return true; + if (buildMinimum(structures.factories, 1)) return true; + if (buildMinimum(structures.gens, 1)) return true; + // make sure trucks go capture some oil at this moment + if (buildMinimumDerricks(1)) return true; + // what if one of them is being upgraded? will need the other anyway. + // also, it looks like the right timing in most cases. + if (buildMinimum(structures.gens, 2)) return true; + if (buildMinimum(structures.hqs, 1)) return true; + // make sure we have at least that much oils by now + if (buildMinimumDerricks(5)) return true; + // support hover maps + var ret = scopeRatings(); + if (ret.land === 0 && !iHaveHover()) + if (buildMinimum(structures.labs, 4)) return true; + if (ret.land === 0 && ret.sea === 0 && !iHaveVtol()) + if (buildMinimum(structures.labs, 4)) return true; + // since we reached that far, we can afford some more trucks + if (gameTime > 300000) { + personality.minTrucks = 5; + personality.maxPower = 300; + personality.minMiscTanks = 2; + personality.minMiscTanks = 6; + // build more factories and labs when we have enough income + if (buildMinimum(structures.labs, derrickCount / 3)) return true; + if (buildMinimum(structures.factories, 2)) return true; + if (buildMinimum(structures.templateFactories, 1)) return true; + if (buildMinimum(structures.vtolFactories, 1)) return true; + return false; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +// Proceed with the main code + +include(HAR_INCLUDES + "_main.js"); diff --git a/harold_generic.json b/harold_generic.json new file mode 100644 index 0000000..0f603c5 --- /dev/null +++ b/harold_generic.json @@ -0,0 +1,7 @@ +{ + "AI": { + "js": "harold_generic.js", + "name": "Harold-git", + "tip": "Harold - NullBot's 'Chatty' brother" + } +}