Skip to content

Instantly share code, notes, and snippets.

@MHeasell
Created April 8, 2015 22:15
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MHeasell/e7ddb00a279d141d03fd to your computer and use it in GitHub Desktop.
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.
// 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
}
};
}
// 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();
}
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']);
});
// 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) } }
}
}
// 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;
}
// 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