Created
April 8, 2015 22:15
-
-
Save MHeasell/e7ddb00a279d141d03fd to your computer and use it in GitHub Desktop.
My CodeCombat Zero Sum tournament entry source code. Check out https://gist.github.com/MHeasell/bb9f7253da45b5140a83 for the compiled version and http://michaelheasell.com/blog/2015/04/08/zero-sum-my-winning-strategy/ for a full write-up about my strategy.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// bin stuff ------------------------------------------------------------------- | |
const BIN_WIDTH = 13.333333333333; | |
const HALF_BIN_WIDTH = (BIN_WIDTH / 2); | |
const BIN_HEIGHT = 14; | |
const HALF_BIN_HEIGHT = (BIN_HEIGHT / 2); | |
const NUM_BINS_X = 6; | |
const NUM_BINS_Y = 5; | |
const TOTAL_BINS = (NUM_BINS_X * NUM_BINS_Y); | |
macro binIndex { | |
rule { ($x, $y) } => { (($y * NUM_BINS_X) + $x) } | |
} | |
macro getBin { | |
rule { ($bins, $x, $y) } => { $bins[binIndex($x, $y)] } | |
} | |
macro binCoords { | |
rule { ($i) } => { | |
{ x: $i % NUM_BINS_X, y: Math.floor($i / NUM_BINS_X) } | |
} | |
} | |
function makeBins() { | |
var bins = new Array(TOTAL_BINS); | |
iter (i from 0 to TOTAL_BINS) { | |
bins[i] = []; | |
} | |
return bins; | |
} | |
function putInBins(items) { | |
var bins = makeBins(); | |
iter (item in items) { | |
var itemPos = item.pos; | |
var binX = Math.floor(itemPos.x / BIN_WIDTH); | |
var binY = Math.floor(itemPos.y / BIN_HEIGHT); | |
// ignore corpses on the outskirts | |
if (binY >= NUM_BINS_Y || binX >= NUM_BINS_X) { | |
continue; | |
} | |
getBin(bins, binX, binY).push(item); | |
} | |
return bins; | |
} | |
function findBestBinPos(bins) { | |
var winIdx; | |
var winScore = -Infinity; | |
iter (i to TOTAL_BINS) { | |
var score = bins[i].length; | |
if (score > winScore) { | |
winScore = score; | |
winIdx = i; | |
} | |
} | |
return {pos: binCoords(winIdx), score: winScore}; | |
} | |
function findBestBinPosAvgCoords(bins) { | |
var winIdx; | |
var winScore = -Infinity; | |
iter (i to TOTAL_BINS) { | |
var score = bins[i].length; | |
if (score > winScore) { | |
winScore = score; | |
winIdx = i; | |
} | |
} | |
var winX = 0; | |
var winY = 0; | |
var winBin = bins[i]; | |
iter (item in winBin) { | |
var itemPos = item.pos; | |
winX += itemPos.x; | |
winY += itemPos.y; | |
} | |
winX /= winBin.length; | |
winY /= winBin.length; | |
return { pos: { x: winX, y: winY }, score: winScore }; | |
} | |
function findLargestGroupPosAvg(items) { | |
var bins = putInBins(items); | |
var data = findBestBinPos(bins); | |
return data; | |
} | |
function findLargestGroupPos(items) { | |
var bins = putInBins(items); | |
var data = findBestBinPos(bins); | |
var pos = data.pos; | |
return { | |
score: data.score, | |
pos: { | |
x: (pos.x * BIN_WIDTH) + HALF_BIN_WIDTH, | |
y: (pos.y * BIN_HEIGHT) + HALF_BIN_HEIGHT | |
} | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// globals --------------------------------------------------------------------- | |
var globals = { | |
enemies: [], | |
filteredEnemies: [], | |
enemySoldiers: [], | |
enemyArtillery: [], | |
enemyArchers: [], | |
enemyGriffins: [], | |
enemySorc: null, | |
items: [], | |
corpses: [], | |
friends: [], | |
friendlySoldiers: [], | |
friendlyArchers: [], | |
friendlyGriffins: [], | |
friendlyArtillery: [], | |
yeti: null, | |
cachedRaiseDeadTime: -Infinity, | |
cachedRaiseDeadData: null, | |
cachedManaBlastTime: -Infinity, | |
cachedManaBlastData: null, | |
lastGoldstormTime: -Infinity, | |
lastSorcFearTime: -Infinity, | |
lastJumpTime: -Infinity | |
}; | |
this.getManaBlastData = function() { | |
var time = this.now(); | |
if (time - globals.cachedManaBlastTime >= 0.5) { | |
globals.cachedManaBlastData = findLargestGroupPosAvg(globals.filteredEnemies); | |
globals.cachedManaBlastTime = time; | |
} | |
return globals.cachedManaBlastData; | |
}; | |
this.getRaiseDeadData = function() { | |
var time = this.now(); | |
if (time - globals.cachedRaiseDeadTime >= 2) { | |
globals.cachedRaiseDeadData = findLargestGroupPos(globals.corpses); | |
globals.cachedRaiseDeadTime = time; | |
} | |
return globals.cachedRaiseDeadData; | |
}; | |
this.updateGlobals = function() { | |
globals.enemies = this.findEnemies(); | |
globals.filteredEnemies = filterNotType("yeti", filterNotType("cage", globals.enemies)); | |
globals.enemySorc = firstOfType("sorcerer", globals.enemies); | |
globals.enemySoldiers = filterType("soldier", globals.enemies); | |
globals.enemyArchers = filterType("archer", globals.enemies); | |
globals.enemyGriffins = filterType("griffin-rider", globals.enemies); | |
globals.enemyArtillery = filterType("artillery", globals.enemies); | |
globals.yeti = firstOfType("yeti", globals.enemies); | |
globals.items = this.findItems(); | |
globals.corpses = this.findCorpses(); | |
globals.friends = this.findFriends(); | |
globals.friendlySoldiers = filterType("soldier", globals.friends); | |
globals.friendlyArchers = filterType("archer", globals.friends); | |
globals.friendlyGriffins = filterType("griffin-rider", globals.friends); | |
globals.friendlyArtillery = filterType("artillery", globals.friends); | |
}; | |
// coin collection algorithm --------------------------------------------------- | |
const ARENA_WIDTH = 85; | |
const ARENA_HEIGHT = 72; | |
const WALL_ATTRACTION_STRENGTH = (-1); | |
const WALL_REPULSION_STRENGTH = (-WALL_ATTRACTION_STRENGTH); | |
const YETI_ATTRACTION_STRENGTH = (-20); | |
const DIRECTION_CASTAHEAD = 10; | |
// Paths to the best coin via the "gravity" approach. | |
this.findGoalCoinPositionGravity = function(coins) { | |
var forceX = 0; | |
var forceY = 0; | |
var pos = this.pos; | |
var posX = pos.x; | |
var posY = pos.y; | |
var enemySorc = globals.enemySorc; | |
var yeti = globals.yeti; | |
var enemyIsFeared = (this.now() - globals.lastSorcFearTime) < 5; | |
iter (coin in coins) { | |
var dist = this.distanceTo(coin); | |
if (dist === 0) { | |
continue; | |
} | |
var enemyDist = enemySorc.distanceTo(coin); | |
var attractionStrength = coin.value; | |
var enemyMultiplier = (dist + enemyDist) / enemyDist; | |
if (enemyMultiplier > 2.22222222) { | |
if (enemyIsFeared) { | |
attractionStrength *= 2.22222222; | |
} | |
else { | |
attractionStrength /= 2; | |
} | |
} | |
else { | |
attractionStrength *= enemyMultiplier; | |
} | |
// Strength is attenuated based on distance squared, | |
// however here we divide by distance again, | |
// making it distance cubed, | |
// because the direction vector is not normalized | |
// and is therefore proportional to distance. | |
var finalStrength = attractionStrength / (dist * dist * dist); | |
var coinPos = coin.pos; | |
forceX += (coinPos.x - posX) * finalStrength; | |
forceY += (coinPos.y - posY) * finalStrength; | |
} | |
// yeti influence | |
if (yeti !== null) { | |
// Strength is attenuated based on distance squared, | |
// however here we divide by distance again, | |
// making it distance cubed, | |
// because the direction vector is not normalized | |
// and is therefore proportional to distance. | |
var yetiDist = this.distanceTo(yeti); | |
if (yetiDist !== 0) { | |
var yetiStrength = YETI_ATTRACTION_STRENGTH / (yetiDist * yetiDist * yetiDist); | |
var yetiPos = yeti.pos; | |
forceX += (yetiPos.x - posX) * yetiStrength; | |
forceY += (yetiPos.y - posY) * yetiStrength; | |
} | |
} | |
// wall influence | |
var leftWallDist = pos.x; | |
var rightWallDist = ARENA_WIDTH - pos.x; | |
forceX += (WALL_REPULSION_STRENGTH / (leftWallDist * leftWallDist)) - (WALL_REPULSION_STRENGTH / (rightWallDist * rightWallDist)); | |
var topWallDist = ARENA_HEIGHT - pos.y; | |
var bottomWallDist = pos.y; | |
forceY += (WALL_REPULSION_STRENGTH / (bottomWallDist * bottomWallDist)) - (WALL_REPULSION_STRENGTH / (topWallDist * topWallDist)); | |
var finalScale = DIRECTION_CASTAHEAD / vecLenM(forceX, forceY); | |
var newPos = { | |
x: posX + (forceX * finalScale), | |
y: posY + (forceY * finalScale) | |
}; | |
return newPos; | |
}; | |
this.findGoalCoinPositionGreedy = function(coins) { | |
var winner = null; | |
var winScore = -Infinity; | |
iter (coin in coins) { | |
var dist = this.distanceTo(coin); | |
var strength = coin.value; | |
if (strength === 3) { | |
strength = 6; | |
} | |
var score = strength / dist; | |
var enemyDist = globals.enemySorc.distanceTo(coin); | |
if (dist > enemyDist) { | |
score /= 2; | |
} | |
if (globals.yeti) { | |
var yetiDist = globals.yeti.distanceTo(coin); | |
if (dist > yetiDist) { | |
score /= 4; | |
} | |
} | |
if (score > winScore) { | |
winner = coin; | |
winScore = score; | |
} | |
} | |
if (winner === null) { | |
return { x: 40, y: 38 }; | |
} | |
return winner.pos; | |
}; | |
this.findGoalCoinPositionGold = function(coins) { | |
var winner = null; | |
var winScore = -Infinity; | |
iter (coin in coins) { | |
var strength = coin.value; | |
if (strength === 3) { | |
strength === 9; | |
} | |
var score = strength/this.distanceTo(coin); | |
if (score > winScore) { | |
winner = coin; | |
winScore = score; | |
} | |
} | |
return winner.pos; | |
}; | |
// utility agent framework ----------------------------------------------------- | |
const FEAR_RANGE = 25; | |
const DRAIN_LIFE_RANGE = 15; | |
const MANA_BLAST_RANGE = 20; | |
const RAISE_DEAD_RADIUS = 20; | |
const ATTACK_RANGE = 40; | |
// actions | |
this.collectCoinAction = function() { | |
return { action: "collect-coins" }; | |
}; | |
this.goldstormAction = function() { | |
if (!this.canCast("goldstorm")) { | |
return null; | |
} | |
if (this.distanceTo(globals.enemySorc) < 40) { | |
return null; | |
} | |
return { action: "goldstorm" }; | |
}; | |
this.raiseDeadAction = function() { | |
if (this.now() < 5) { | |
return null; | |
} | |
if (!this.canCast("raise-dead") && !this.isReady("reset-cooldown")) { | |
return null; | |
} | |
var nearbyCorpses = this.findInRadius(RAISE_DEAD_RADIUS, globals.corpses); | |
// do not res artillery, it does more harm than good | |
if (firstOfType("artillery", nearbyCorpses) !== null) { | |
return null; | |
} | |
// don't waste the spell for too few corpses | |
if (nearbyCorpses.length < 2) { | |
return null; | |
} | |
if (!this.canCast("raise-dead")) { | |
return { action: "reset-cooldown", target: "raise-dead" }; | |
} | |
else { | |
return { action: "raise-dead" }; | |
} | |
}; | |
this.attackArchersAction = function() { | |
var targetArcher = findLowestHealth(this.findInRadius(25, globals.enemyArchers)); | |
if (targetArcher === null) { | |
return null; | |
} | |
return { action: "attack", target: targetArcher }; | |
} | |
this.attackSorcInEndGameAction = function() { | |
if (this.now() < 110) { | |
return null; | |
} | |
var sorc = globals.enemySorc; | |
if (sorc.health >= this.health) { | |
return null; | |
} | |
return { action: "attack", target: globals.enemySorc }; | |
} | |
this.attackArtilleryAction = function() { | |
if (globals.enemyArtillery.length < 1) { | |
return null; | |
} | |
var target = this.findNearest(globals.enemyArtillery); | |
if (target === null) { | |
return null; | |
} | |
if (this.distanceTo(target) >= ATTACK_RANGE) { | |
return null; | |
} | |
return { action: "attack", target: target }; | |
}; | |
this.aggressiveManaBlastAction = function() { | |
if (this.now() < 5) { | |
return null; | |
} | |
if (!this.isReady("mana-blast") && !this.isReady("reset-cooldown")) { | |
return null; | |
} | |
var manaBlastData = this.getManaBlastData(); | |
var enemyCount = manaBlastData.score; | |
// don't waste the spell on too few enemies | |
if (enemyCount <= 3) { | |
return null; | |
} | |
var manaBlastPos = manaBlastData.pos; | |
var distance = vecDist(this.pos, manaBlastPos); | |
if (distance > 10) { | |
return null; | |
} | |
if (!this.isReady("mana-blast")) { | |
return { action: "reset-cooldown", target: "mana-blast" }; | |
} | |
else if (distance > 3) { | |
return { action: "move", target: manaBlastPos }; | |
} | |
else { | |
return { action: "mana-blast" }; | |
} | |
}; | |
this.defensiveManaBlastAction = function() { | |
if (!this.isReady("mana-blast") && !this.isReady("reset-cooldown")) { | |
return null; | |
} | |
var nearEnemies = this.findInRadius(10, globals.filteredEnemies); | |
if (nearEnemies.length < 5) { | |
return null; | |
} | |
if (!this.isReady("mana-blast")) { | |
return { action: "reset-cooldown", target: "mana-blast" }; | |
} | |
else { | |
return { action: "mana-blast" }; | |
} | |
}; | |
this.drainLifeAction = function() { | |
if (!this.canCast("drain-life")) { | |
return null; | |
} | |
if (this.health / this.maxHealth > 0.3) { | |
return null; | |
} | |
var target = findLowestHealth(this.findInRadius(DRAIN_LIFE_RANGE, globals.enemies)); | |
if (target === null) { | |
return null; | |
} | |
return { action: "drain-life", target: target }; | |
}; | |
this.fearEnemySorcererAction = function() { | |
if (this.canCast("fear")) { | |
if (!this.canCast("fear", globals.enemySorc)) { | |
return null; | |
} | |
} | |
else if (!this.isReady("reset-cooldown")) { | |
return null; | |
} | |
if (this.distanceTo(globals.enemySorc) > FEAR_RANGE + 10) { | |
return null; | |
} | |
if (!this.canCast("fear")) { | |
return { action: "reset-cooldown", target: "fear" }; | |
} | |
else { | |
return { action: "fear", target: globals.enemySorc }; | |
} | |
}; | |
this.fearYetiAction = function() { | |
if (globals.yeti === null) { | |
return null; | |
} | |
if (this.canCast("fear")) { | |
if (!this.canCast("fear", globals.yeti)) { | |
return null; | |
} | |
} | |
else if (!this.isReady("reset-cooldown")) { | |
return null; | |
} | |
if (this.distanceTo(globals.yeti) > 20) { | |
return null; | |
} | |
if (globals.yeti.target !== this) { | |
return null; | |
} | |
if (!this.canCast("fear")) { | |
return { action: "reset-cooldown", target: "fear" }; | |
} | |
else { | |
return { action: "fear", target: globals.yeti }; | |
} | |
}; | |
this.avoidYetiAction = function() { | |
if (!globals.yeti) { | |
return null; | |
} | |
if (this.distanceTo(globals.yeti) > 20) { | |
return null; | |
} | |
// the coin collection algorithm devalues coins near the yeti | |
// so it should steer us away. | |
return { action: "collect-coins" }; | |
}; | |
// action selection and execution logic | |
this.goToTarget = function(pos) { | |
pos = this.pathAroundBox(pos); | |
if (this.isReady("jump") && vecDist(this.pos, pos) >= 8) { | |
this.jumpTo(pos); | |
globals.lastJumpTime = this.now(); | |
} | |
else { | |
this.move(pos); | |
} | |
}; | |
const BOX_WIDTH = 8; | |
const BOX_HALF_WIDTH = (BOX_WIDTH/2); | |
const BOX_HEIGHT = 8; | |
const BOX_HALF_HEIGHT = (BOX_HEIGHT/2); | |
const BOX_PATH_CASTAHEAD = 10; | |
this.pathAroundBox = function(pos) { | |
var box = firstOfType('cage', globals.enemies); | |
if (box === null) { | |
return pos; | |
} | |
if (this.distanceTo(box) > 7) { | |
return pos; | |
} | |
if ((this.pos.y < box.pos.y && pos.y > box.pos.y) || (this.pos.y > box.pos.y && pos.y < box.pos.y)) { | |
// our path crosses the box's y position | |
if (this.pos.x <= box.pos.x + BOX_HALF_WIDTH && this.pos.x >= box.pos.x - BOX_HALF_WIDTH) { | |
// we are overlapping the box on the x axis | |
if (pos.x > box.pos.x) { | |
// target is on the right, move right | |
return { x: pos.x + BOX_PATH_CASTAHEAD, y: pos.y }; | |
} | |
else { | |
// target is on the left, move left | |
return {x: pos.x - BOX_PATH_CASTAHEAD, y: pos.y }; | |
} | |
} | |
} | |
if ((this.pos.x < box.pos.x && pos.x > box.pos.x) || (this.pos.x > box.pos.x && pos.y < box.pos.x)) { | |
// our path crosses the box's x position | |
if (this.pos.y <= box.pos.y + BOX_HALF_HEIGHT && this.pos.y >= box.pos.y - BOX_HALF_HEIGHT) { | |
// we are overlapping the box on the y axis | |
if (pos.y > box.pos.y) { | |
// target is above, move up | |
return { x: pos.x, y: pos.y + BOX_PATH_CASTAHEAD }; | |
} | |
else { | |
// target is below, move down | |
return {x: pos.x, y: pos.y - BOX_PATH_CASTAHEAD }; | |
} | |
} | |
} | |
return pos; | |
} | |
this.executeAction = function(action) { | |
switch (action.action) { | |
case "move": | |
this.goToTarget(action.target); | |
return; | |
case "collect-coins": | |
var coinPos; | |
var timeSinceGS = this.now() - globals.lastGoldstormTime; | |
if (timeSinceGS > 0.5 && timeSinceGS < 10) { | |
coinPos = this.findGoalCoinPositionGold(globals.items); | |
} | |
else { | |
coinPos = this.findGoalCoinPositionGravity(globals.items); | |
} | |
this.goToTarget(coinPos); | |
return; | |
case "attack": | |
this.attack(action.target); | |
return; | |
case "raise-dead": | |
this.cast("raise-dead"); | |
return; | |
case "mana-blast": | |
this.manaBlast(); | |
return; | |
case "drain-life": | |
this.cast("drain-life", action.target); | |
return; | |
case "fear": | |
this.cast("fear", action.target); | |
if (action.target === globals.enemySorc) { | |
globals.lastSorcFearTime = this.now(); | |
} | |
return; | |
case "goldstorm": | |
this.cast("goldstorm"); | |
globals.lastGoldstormTime = this.now(); | |
return; | |
case "reset-cooldown": | |
this.resetCooldown(action.target); | |
return; | |
default: | |
this.debug("How do I " + action.type + "?"); | |
return; | |
} | |
}; | |
macro selectActionM { | |
rule { | |
{ $($func:expr (,) ...) } | |
} => { | |
var action; | |
$( | |
action = $func(); | |
if (action !== null) { | |
return action; | |
} | |
) | |
... | |
} | |
} | |
this.selectAction = function() { | |
selectActionM { | |
//this.fearYetiAction, | |
//this.drainLifeAction, | |
//this.fearEnemySorcererAction, | |
this.avoidYetiAction, | |
this.raiseDeadAction, | |
this.aggressiveManaBlastAction, | |
this.defensiveManaBlastAction, | |
//this.attackArchersAction, | |
this.attackArtilleryAction, | |
this.attackSorcInEndGameAction, | |
this.goldstormAction, | |
this.collectCoinAction, | |
} | |
}; | |
// ability use and movement ---------------------------------------------------- | |
this.doAbilityLogic = function() { | |
this.executeAction(this.selectAction()); | |
}; | |
// army summoning -------------------------------------------------------------- | |
this.percentageSummon = function() { | |
var griffinCount = globals.friendlyGriffins.length; | |
var soldierCount = globals.friendlySoldiers.length; | |
if (griffinCount < 3) { | |
return "griffin-rider"; | |
} | |
if (soldierCount < 5) { | |
return "soldier"; | |
} | |
if (soldierCount / griffinCount < 0.666666) { | |
return "soldier"; | |
} | |
return "griffin-rider"; | |
}; | |
this.decideSummon = function() { | |
//if (this.now() > 30) { | |
// return "griffin-rider"; | |
//} | |
var nearestEnemy = this.findNearest(globals.filteredEnemies); | |
// spawn troops if the enemy is near. | |
// if it's the early game, don't spawn unless they have military | |
// (i.e. more than just the sorc) | |
if (this.distanceTo(nearestEnemy) < 30 && (this.now() > 45 || globals.filteredEnemies.length > 1)) { | |
return this.percentageSummon(); | |
} | |
if (this.shouldDoEndGameAttack()) { | |
return this.percentageSummon(); | |
} | |
if (this.shouldGoOffensive()) { | |
return this.percentageSummon(); | |
} | |
if (this.gold >= 150) { | |
return "griffin-rider"; | |
} | |
return null; | |
}; | |
this.shouldDoEndGameAttack = function() { | |
return this.now() > 105 && globals.friends.length > globals.filteredEnemies.length; | |
}; | |
this.shouldGoOffensive = function() { | |
return globals.friends.length > 9 && globals.friends.length / globals.filteredEnemies.length > 2; | |
} | |
this.doSummonLogic = function() { | |
var summonType = this.decideSummon(); | |
// if we summon while jumping we seem to always crash due to | |
// "cannot read property z of null" | |
var timeSinceJump = this.now() - globals.lastJumpTime; | |
if (timeSinceJump > 0.5) { | |
while (summonType !== null && this.gold > this.costOf(summonType)) { | |
this.summon(summonType); | |
this.updateGlobals(); | |
summonType = this.decideSummon(); | |
} | |
return true; | |
} | |
return false; | |
}; | |
// troop orders ---------------------------------------------------------------- | |
macro commandAttack { | |
rule { ($target, $units); } => { | |
var len = $units.length; | |
iter (i from 0 to len) { | |
this.command($units[i], "attack", $target); | |
} | |
} | |
} | |
macro commandMove { | |
rule { ($pos, $units); } => { | |
var len = $units.length; | |
iter (i from 0 to len) { | |
this.command($units[i], "move", $pos); | |
} | |
} | |
} | |
this.doAttackNearestAI = function(enemies, units) { | |
iter (unit in units) { | |
var nearestEnemy = unit.findNearest(enemies); | |
this.command(unit, "attack", nearestEnemy); | |
} | |
}; | |
this.selectRoamTarget = function(unit) { | |
var target = unit.findNearest(globals.enemyArtillery); | |
if (target !== null) { | |
return target; | |
} | |
target = unit.findNearest(globals.enemyArchers); | |
if (target !== null) { | |
return target; | |
} | |
target = unit.findNearest(globals.enemyGriffins); | |
if (target !== null) { | |
return target; | |
} | |
//target = unit.findNearest(globals.enemySoldiers); | |
//if (target !== null) { | |
// return target; | |
//} | |
return globals.enemySorc; | |
}; | |
this.pickRandomPosition = function() { | |
return { | |
x: Math.random() * 83, | |
y: Math.random() * 70 | |
}; | |
}; | |
this.maybeKiteSoldiers = function(leader, units) { | |
var leaderPos = leader.pos; | |
var nearestSoldier = leader.findNearest(globals.enemySoldiers); | |
if (nearestSoldier !== null) { | |
var soldierDistance = leader.distanceTo(nearestSoldier); | |
if (soldierDistance < 10) { | |
var soldierPos = nearestSoldier.pos; | |
var offsetScale = (soldierDistance === 0)?10:(10 / soldierDistance); | |
var newPos = { | |
x: leaderPos.x + ((leaderPos.x - soldierPos.x) * offsetScale), | |
y: leaderPos.y + ((leaderPos.y - soldierPos.y) * offsetScale) | |
}; | |
commandMove(newPos, units); | |
return true; | |
} | |
} | |
return false; | |
}; | |
this.doRoamAttackAI = function(units) { | |
if (units.length === 0) { | |
return; | |
} | |
var leader = units[0]; | |
if (this.maybeKiteSoldiers(leader, units)) { | |
return; | |
} | |
var target = this.selectRoamTarget(leader); | |
commandAttack(target, units); | |
}; | |
this.doAttackTargetAndKiteAI = function(target, units) { | |
if (units.length === 0) { | |
return; | |
} | |
var leader = units[0]; | |
if (this.maybeKiteSoldiers(leader, units)) { | |
return; | |
} | |
commandAttack(target, units); | |
}; | |
this.doAttackTargetAI = function(target, units) { | |
if (units.length === 0) { | |
return; | |
} | |
commandAttack(target, units); | |
}; | |
this.doAttackSpecificAI = function(units) { | |
if (units.length === 0) { | |
return; | |
} | |
var target = units[0].findNearest(globals.filteredEnemies); | |
commandAttack(target, units); | |
}; | |
this.maybeKiteIndividual = function(unit, enemies) { | |
var nearestSoldier = unit.findNearest(enemies); | |
if (nearestSoldier !== null) { | |
var soldierDistance = unit.distanceTo(nearestSoldier); | |
if (soldierDistance < 6) { | |
var soldierPos = nearestSoldier.pos; | |
var offsetScale = (soldierDistance === 0)?10:(10 / soldierDistance); | |
var unitPos = unit.pos; | |
var newPos = { | |
x: unitPos.x + ((unitPos.x - soldierPos.x) * offsetScale), | |
y: unitPos.y + ((unitPos.y - soldierPos.y) * offsetScale) | |
}; | |
this.command(unit, "move", newPos); | |
return true; | |
} | |
} | |
return false; | |
} | |
this.doDefensiveGriffinAI = function(units) { | |
var enemySoldiers = globals.enemySoldiers; | |
var heroPos = this.pos; | |
var offsetHeroPos = vecAddSM(heroPos, 2); | |
var yeti = globals.yeti; | |
var yetiArr = [yeti]; | |
var artillery = (globals.enemyArtillery.length > 0)?globals.enemyArtillery[0]:null; | |
if (artillery !== null && enemySoldiers.length === 0) { | |
commandAttack(artillery, units); | |
return; | |
} | |
iter (unit in units) { | |
if (this.maybeKiteIndividual(unit, enemySoldiers)) { | |
continue; | |
} | |
if (yeti !== null && this.maybeKiteIndividual(unit, yetiArr)) { | |
continue; | |
} | |
var lowestHealthEnemy = findLowestHealth(findInTargetRadius(unit, 20, globals.filteredEnemies)); | |
if (lowestHealthEnemy !== null) { | |
this.command(unit, "attack", lowestHealthEnemy); | |
continue; | |
} | |
this.command(unit, "move", offsetHeroPos); | |
} | |
}; | |
this.doOffensiveGriffinAI = function(units) { | |
var enemySoldiers = globals.enemySoldiers; | |
var enemyRanged = globals.enemyGriffins.concat( | |
globals.enemyArchers, | |
globals.enemyArtillery); | |
var sorc = globals.enemySorc; | |
var yeti = globals.yeti; | |
var yetiArr = [yeti]; | |
iter (unit in units) { | |
if (this.maybeKiteIndividual(unit, enemySoldiers)) { | |
continue; | |
} | |
if (yeti !== null && this.maybeKiteIndividual(unit, yetiArr)) { | |
continue; | |
} | |
// prioritize ranged units, they are a threat to us | |
var lowestHealthRanged = findLowestHealth(findInTargetRadius(unit, 20, enemyRanged)); | |
if (lowestHealthRanged !== null) { | |
this.command(unit, "attack", lowestHealthRanged); | |
continue; | |
} | |
// kill the sorc if in range | |
if (unit.distanceTo(sorc) < 20) { | |
this.command(unit, "attack", sorc); | |
continue; | |
} | |
// otherwise clean up soldiers | |
var lowestHealthSoldier = findLowestHealth(findInTargetRadius(unit, 20, enemySoldiers)); | |
if (lowestHealthSoldier !== null) { | |
this.command(unit, "attack", lowestHealthSoldier); | |
continue; | |
} | |
var nearestEnemy = this.findNearest(globals.filteredEnemies); | |
this.command(unit, "attack", nearestEnemy); | |
} | |
}; | |
this.doDefensiveTroopLogic = function() { | |
this.doAttackNearestAI(globals.filteredEnemies, globals.friendlySoldiers); | |
this.doDefensiveGriffinAI(globals.friendlyGriffins); | |
}; | |
this.doOffensiveTroopLogic = function() { | |
this.doAttackNearestAI(globals.enemies, globals.friendlySoldiers); | |
this.doOffensiveGriffinAI(globals.friendlyGriffins); | |
}; | |
this.doEndGameTroopLogic = function() { | |
this.doAttackTargetAI(globals.enemySorc, globals.friends); | |
}; | |
this.doTroopLogic = function() { | |
//var grifCount = globals.friendlyGriffins.length; | |
//var enemyGrifCount = globals.enemyGriffins.length; | |
//if (grifCount > 4 && grifCount / enemyGrifCount >= 1.8) { | |
// this.doOffensiveTroopLogic(); | |
//} | |
//else { | |
// this.doDefensiveTroopLogic(); | |
//} | |
if (this.shouldDoEndGameAttack()) { | |
this.doEndGameTroopLogic(); | |
} | |
else if (this.shouldGoOffensive()) { | |
this.doOffensiveTroopLogic(); | |
} | |
else { | |
this.doDefensiveTroopLogic(); | |
} | |
}; | |
// main loop ------------------------------------------------------------------- | |
// There's some broken player floating around who spawns tharin, | |
// the knight, instead of their sorc. | |
// Just kill this guy. | |
if (this.findEnemies()[0].type === "knight") { | |
for (;;) { | |
this.attack(this.findEnemies()[0]); | |
} | |
} | |
for (;;) { | |
this.updateGlobals(); | |
if (globals.enemySorc === null) { | |
return; | |
} | |
// command troops | |
this.doTroopLogic(); | |
// "emergency" actions that take priority over summoning | |
var action; | |
action = this.fearYetiAction(); | |
if (action !== null) { | |
this.executeAction(action); | |
continue; | |
} | |
action = this.fearEnemySorcererAction(); | |
if (action !== null) { | |
this.executeAction(action); | |
continue; | |
} | |
// summon troops | |
this.doSummonLogic(); | |
// use abilities and collect coins | |
this.doAbilityLogic(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var gulp = require('gulp'); | |
var replace = require('gulp-replace'); | |
var sweet = require('gulp-sweetjs'); | |
var rename = require('gulp-rename'); | |
var del = require('del'); | |
var concat = require('gulp-concat'); | |
var files = [ | |
'macros.js', | |
'vector.js', | |
'bins.js', | |
'util.js', | |
'code.js', | |
]; | |
gulp.task('build', function() { | |
gulp.src(files) | |
.pipe(concat('all.js')) | |
.pipe(sweet({readableNames: true})) | |
.pipe(replace(/for \(;;\) \{/, "loop {")) | |
.pipe(rename("output.js")) | |
.pipe(gulp.dest('.')); | |
}); | |
gulp.task('watch', ['build'], function() { | |
gulp.watch(files, ['build']); | |
}); | |
gulp.task('default', ['build'], function() { | |
}); | |
gulp.task('clean', function() { | |
del(['output.js']); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// macros ---------------------------------------------------------------------- | |
macro iter { | |
rule { | |
($ident in $coll) { $body ... } | |
} => { | |
iter (i, $ident in $coll) { | |
$body ... | |
} | |
} | |
rule { | |
($idx, $ident in $coll) { $body ... } | |
} => { | |
for (var $idx = 0, len = $coll.length; $idx < len; ++$idx) { | |
var $ident = $coll[$idx]; | |
$body ... | |
} | |
} | |
rule { | |
($ident to $stop) { $body ... } | |
} => { | |
iter($ident from 0 to $stop) { | |
$body ... | |
} | |
} | |
rule { | |
($ident from $start to $stop) { $body ... } | |
} => { | |
for (var $ident = $start; $ident < $stop; ++$ident) { | |
$body ... | |
} | |
} | |
} | |
macro constexpr { | |
case { _($e:expr) } => { | |
return localExpand(#{ | |
macro cexpr { | |
case { _ } => { | |
return [makeValue($e, #{ here })]; | |
} | |
} | |
cexpr | |
}); | |
} | |
} | |
macro const { | |
rule { | |
$ident:ident = $val:expr; | |
} => { | |
macro $ident { rule {} => { constexpr($val) } } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// utility functions ----------------------------------------------------------- | |
// Filters the given array of entities to those of the given type | |
function filterType(type, arr) { | |
var newArr = []; | |
iter (item in arr) { | |
if (item.type === type) { | |
newArr.push(item); | |
} | |
} | |
return newArr; | |
} | |
function filterNotType(type, arr) { | |
var newArr = []; | |
iter (item in arr) { | |
if (item.type !== type) { | |
newArr.push(item); | |
} | |
} | |
return newArr; | |
} | |
this.filterAllies = function(arr) { | |
var newArr = []; | |
iter (item in arr) { | |
if (item.team === this.team) { | |
newArr.push(item); | |
} | |
} | |
return newArr; | |
}; | |
function firstOfType(type, arr) { | |
iter (ent in arr) { | |
if (ent.type === type) { | |
return ent; | |
} | |
} | |
return null; | |
} | |
this.findInRadius = function(radius, arr) { | |
var items = []; | |
iter (item in arr) { | |
if (this.distanceTo(item) < radius) { | |
items.push(item); | |
} | |
} | |
return items; | |
}; | |
function findInTargetRadius(target, radius, arr) { | |
var items = []; | |
iter (item in arr) { | |
if (target.distanceTo(item) < radius) { | |
items.push(item); | |
} | |
} | |
return items; | |
} | |
function findLowestHealth(units) { | |
var winner = null; | |
var winScore = Infinity; | |
iter (u in units) { | |
var score = u.health; | |
if (score < winScore) { | |
winner = u; | |
winScore = score; | |
} | |
} | |
return winner; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// vector utilities ------------------------------------------------------------ | |
macro vecAddM { | |
rule { ($a:expr, $b:expr) } => { { x: $a.x + $b.x, y: $a.y + $b.y } } | |
} | |
macro vecAddSM { | |
rule { ($a:expr, $s:expr) } => { { x: $a.x + $s, y: $a.y + $s } } | |
} | |
function vecAdd(a, b) { | |
return vecAddM(a, b); | |
} | |
macro vecSubM { | |
rule { ($a:expr, $b:expr) } => { { x: $a.x - $b.x, y: $a.y - $b.y } } | |
} | |
function vecSub(a, b) { | |
return vecSubM(a, b); | |
} | |
macro vecMulM { | |
rule { ($v:expr, $s:expr) } => { { x: $v.x * $s, y: $v.y * $s } } | |
} | |
function vecMul(v, s) { | |
return vecMulM(v, s); | |
} | |
macro vecLenM { | |
rule { ($v:expr) } => { vecLenM($v.x, $v.y) } | |
rule { ($x:expr, $y:expr) } => { Math.sqrt(($x * $x) + ($y * $y)) } | |
} | |
function vecLen(v) { | |
return vecLenM(v); | |
} | |
function vecNorm(v) { | |
var len = vecLenM(v); | |
if (len === 0) { | |
return { x: 1, y: 1 }; | |
} | |
return { x: v.x / len, y: v.y / len }; | |
} | |
// returns the direction from a to b | |
macro vecDirM { | |
rule { ($a:expr, $b:expr) } => { vecNorm(vecSubM($b, $a)) } | |
} | |
function vecDir(a, b) { | |
return vecDirM(a, b); | |
} | |
function vecDist(a, b) { | |
var x = b.x - a.x; | |
var y = b.y - a.y; | |
return vecLenM(x, y); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment