Skip to content

Instantly share code, notes, and snippets.

@schmatz
Created June 12, 2014 18:52
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save schmatz/fcc04d92473c179b4a28 to your computer and use it in GitHub Desktop.
Save schmatz/fcc04d92473c179b4a28 to your computer and use it in GitHub Desktop.
Ian Elliott's CodeCombat Greed code
var base = this;
if ( !base.init ) {
// Mark that we've ran the init code
base.init = true;
// Starting frame number
base.frames = 0;
// Amount of frames per second
base.frameRate = 4;
// Map unit types so we don't have to worry about them in game
var ogreUnits = { worker: 'peon', soldier: 'munchkin', knight: 'ogre', caster: 'shaman', flyer: 'fangrider', heavy: 'brawler' },
humanUnits = { worker: 'peasant', soldier: 'soldier', knight: 'knight', caster: 'librarian', flyer: 'griffin-rider', heavy: 'captain' };
// Mapping will always refer to the appropriate unit
base.units = (base.id == 'Ogre Base') ?
{ me: ogreUnits, enemy: humanUnits } :
{ me: humanUnits, enemy: ogreUnits };
base.cost = {
worker: 50, solider: 10, knight: 25, caster: 40, flyer: 60, heavy: 100,
peon: 50, munchkin: 10, ogre: 25, shaman: 40, fangrider: 60, brawler: 100,
peasant: 50, soldier: 10, librarian: 40, 'griffin-rider': 60, captain: 100
};
// Map enemy units back to their equivalent of my unit
base.unitsMap = {};
for (var unitName in base.units.enemy) {
base.unitsMap[base.units.enemy[unitName]] = base.units.me[unitName];
}
// Spawn first worker
base.build(base.units.me.worker);
base.prevItems = [];
base.currentItems = [];
base.lastCoin = 1;
base.coinName = 'Hybrid Coin ';
base.coins = 0;
base.maxBuckets = 0;
base.maxCoins = 0;
base.hybridCoins = [];
base.hybridCoinMap = {};
base.centroids = {};
base.centroidList = [];
base.totalCoins = 0;
base.aggressor = 0;
base.launch = false;
base.knights = false;
base.builds = {};
base.startFrame = -1;
base.buildQueue = [];
base.queueCost = 0;
base.queueCaster = false;
base.ignoreEnemies = {};
base.lost = false;
base.armyValue = 0;
base.shouldCancel = false;
// Keeps track of which coins map to the same hybrid coins
base.sameCoins = {};
// Placeholder coin that we never want to reach
base.distantCoin = new Vector(5000,5000);
base.distantCoin.value = 0;
base.distantCoin.magnitude = 'Distant Coin';
var ogreStart = new Vector(75, 60),
humanStart = new Vector(10, 10);
base.myStart = (base.id == 'Ogre Base') ? ogreStart: humanStart;
base.enemyStart = (base.id == 'Ogre Base') ? humanStart: ogreStart;
base.inf = 1/0;
base.lastHealth = base.health;
base.lastArmySize = 0;
base.enemyArmyValue = 0;
base.myArmyValue = 0;
base.inBetweenCoins = {};
} else {
// Remap variables
var hybridCoins = base.hybridCoins,
hybridCoinMap = base.hybridCoinMap,
coinsToAvoid = {},
centroids = base.centroids,
centroidList = base.centroidList,
activeWorkers = {},
enemyCentroids = {};
var distantCoin = base.distantCoin;
var inf = base.inf;
// Update the maps for current and previous keys
var prevItems = base.currentItems,
prevItemCount = prevItems.length,
currentItems = base.currentItems = base.getItems(),
currentItemCount = currentItems.length;
var workerDist = {},
workerCoins = {};
var myWorkers = base.getByType(base.units.me.worker),
myWorkerVectors = [],
enemyWorkerVectors = [],
myWorkerCount = myWorkers.length,
myArmySize = (base.getFriends().length - myWorkerCount),
enemyWorkers = base.getByType(base.units.enemy.worker),
enemyWorkerCount = enemyWorkers.length,
workers = enemyWorkers.concat(myWorkers),
enemies = base.getEnemies(),
cost = base.cost;
var underAttack = (enemies.length - 1) > enemyWorkerCount,
aggressor = base.aggressor;
var indexOf = _.indexOf,
min = Math.min,
max = Math.max,
ceil = Math.ceil,
floor = Math.floor,
abs = Math.abs;
var now = base.now(),
lateGame = now >= 42 && myWorkerCount >= 3,
endGame = false, //now >= 140,
rush = enemyWorkerCount === 0,
dying = base.health < base.lastHealth;
var myGold = base.gold,
enemyGold = enemies[0].gold;
var sizeX = 28.333333,
sizeY = 35,
bucketCount = 2;
var maxWorkers = max(7, min(enemyWorkerCount, 8));
// ------------------------------------------------------------
// Combat
// ------------------------------------------------------------
// Determine my army size
if (myArmySize != base.lastArmySize) {
var myWorkerType = base.units.me.worker,
thisArmyValue = 0;
(base.getFriends()).forEach(function(friend) {
var type = friend.type;
if (type != myWorkerType) {
thisArmyValue += cost[type];
}
});
base.myArmyValue = thisArmyValue;
}
if ( ((underAttack || now >= 125) && !base.launch && base.buildQueue.length === 0) || lateGame ) {
var unitTypes = base.units.enemy,
workerType = unitTypes.worker,
caster = unitTypes.caster,
flyer = unitTypes.flyer,
enemyVectors = [],
rangedVectors = [],
myStart = base.myStart,
enemyStart = base.enemyStart,
armyValue = 0,
enemiesClone = enemies.slice(0),
lastId;
// Who cares about their base...
enemiesClone.shift();
// Get vectors for each enemy
enemiesClone.forEach(function(enemy) {
var type = enemy.type;
if (type != workerType) {
lastId = enemy.id;
// Type data
var enemyVector = new Vector(enemy.pos.x, enemy.pos.y);
enemyVector.dot = type;
enemyVector.magnitude = lastId;
enemyVectors.push(enemyVector);
armyValue += cost[type];
if (type == caster || type == flyer)
rangedVectors.push(enemyVector);
}
});
base.enemyArmyValue = armyValue;
var nearestCaster = base.getNearest(rangedVectors),
nearestEnemy = base.getNearest(enemyVectors),
aggressive = !underAttack && ((!nearestCaster || myStart.distance(nearestCaster) > 70) && (!nearestEnemy || myStart.distance(nearestEnemy) > 30));
// Make sure this is a new threat
if ( (((underAttack && base.enemyArmyValue > 20)|| now >= 125) && !base.launch && base.buildQueue.length === 0) && !base.ignoreEnemies[lastId] ) {
// If under attack and the nearest caster is less than 50 units away, or the nearest melee is less than 30 away
if ( (underAttack && ((nearestCaster && myStart.distance(nearestCaster) <= 70) || (nearestEnemy && myStart.distance(nearestEnemy) <= 30))) || (now >= 125 && (!aggressive || (myGold - 40) > enemyGold)) || (enemyArmyValue >= 50 && myGold <= enemyArmyValue) ) {
// Small scale battle, invested less than 50 minerals - lets build the exact same
// Or I'm broke and losing hard...
if ( (armyValue <= 65 && rangedVectors.length <= 1) && base.buildQueue.length === 0 && now < 125) {
var rangedCost = 0,
rangedUnit;
if (rangedVectors.length) {
rangedCost = cost[rangedVectors[0].dot];
rangedUnit = base.unitsMap[rangedVectors[0].dot];
} else if (base.getByType(base.units.enemy.soldier).length > 4) {
rangedCost = 40;
rangedUnit = base.units.me.caster;
}
var count = max(floor((armyValue - rangedCost) / 10), 0);
base.queueCost = count * 10 + rangedCost;
// Spawn basic workers if they have no casters... mass basic units always beat the other things
while (count--) {
base.buildQueue.push([base.units.me.soldier, base.frames + count]);
if ( (base.buildQueue.length == 1 && !count) || base.buildQueue.length == 2) {
if (rangedCost) {
base.queueCaster = true;
base.buildQueue.push([rangedUnit, base.frames + 10]);
} else {
base.queueCaster = false;
}
}
}
enemyVectors.forEach(function(e) {
// Ignore enemiesClone so that we don't create a new response to an old thread
base.ignoreEnemies[e.magnitude] = true;
});
} else {
// Don't be aggressive with someone we can't beat... stalemate him
if ( !aggressive || (myGold - 40) > enemyGold) {
base.launch = true;
base.startFrame = base.frames;
if (myGold >= 130)
base.knights = true;
}
}
}
} else {
underAttack = false;
}
}
// ------------------------------------------------------------
// Build Units
// ------------------------------------------------------------
// Care !
// If we try to build an item we can't afford, it will be built as soon as possible on any later turn
// This may end in situations where we can't /not/ produce a unit we no longer want
// We can try and back out of it by attempting to produce a unit we can't afford... but if we can afford all units
// then all we can do is try to produce the cheapest one to reduce the effect of the error
// If we're ahead in battle, make workers
if (base.myArmyValue && (base.myArmyValue + myGold - 50 >= base.enemyArmyValue + enemyGold) && myWorkerCount < maxWorkers) {
base.build(base.units.me.worker);
// If dying, spawn workers as required
} else if (dying && base.myArmyValue < base.enemyArmyValue) {
if ( base.getByType(base.units.me.soldier).length > 4 && myGold >= 40 )
base.build(base.units.me.caster);
else
base.build(base.units.me.soldier);
// Fizz buzz of battle
} else if (base.launch) {
var soldierType = base.knights? base.units.me.knight: base.units.me.soldier,
frame = base.frames - base.startFrame,
soldiers = base.getByType(soldierType),
basics = base.getByType(base.units.me.soldier),
casterCount = base.getByType(base.units.me.caster).length,
soldierCount = soldiers.length,
soldierDist = inf,
basicDist = inf;
if (base.knights && soldierCount >= 2) {
soldierCount += base.getByType(base.units.me.soldier).length;
}
if (soldierCount) {
soldierDist = soldiers[0].pos.distance(base.myStart);
}
if (basics.length) {
basicDist = Math.min(basics[0].pos.distance(base.myStart), soldierDist);
}
// We're far ahead... build soldiers
if ( base.myArmyValue >= (base.enemyArmyValue * 1.3) && base.myArmyValue > 140 && rangedVectors && rangedVectors.length < 2 && soldierCount < 6) {
base.build(base.units.me.soldier);
} else if (base.getByType(base.units.enemy.caster).length && casterCount === 0 && soldierCount >= 2) {
base.build(base.units.me.caster);
} else if (basicDist >= 20 && soldierDist >= 20 && rangedVectors && rangedVectors.length) {
base.build(base.units.me.soldier);
// Every 7 frames build a worker
} else if ( soldierCount < 2 || (((frame % 8) === 0 || soldierDist > 10) && (soldierCount < 4)) ) {
if (base.knights && soldierCount >= 2)
base.build(base.units.me.soldier);
else
base.build(soldierType);
// Every frame after build a caster, until next warrior needed
} else if ( frame >= 12 ) {
if ( casterCount > 3 && base.getByType(base.units.me.flyer).length === 0 )
base.build(base.units.me.flyer);
else
base.build(base.units.me.caster);
}
// A unit queue identical to what the enemy sent, should be relatively low value
} else if (base.buildQueue.length) {
if ( !base.queueCaster || base.queueCaster && base.queueCost <= myGold ) {
// Get the first item off the queue without destroying it, in case we can't follow
var queue = base.buildQueue.slice(0),
next = queue.shift();
// Check if its time to build the unit yet, and check if we can build the unit
if (next[1] <= base.frames && myGold >= base.cost[next[0]]) {
base.build(next[0]);
// Actually remove the unit from the queue
base.buildQueue.shift();
base.buildQueue = base.buildQueue.slice(0);
}
}
// Make sure our enemy doesn't try to all in by under producing workers
} else if ((!underAttack || base.enemyArmyValue <= 30) && myWorkerCount < maxWorkers && myGold > 40 && ((myWorkerCount - enemyWorkerCount <= 1 && enemyGold < 50)|| enemyGold < 50)) {
base.build(base.units.me.worker);
// Cancel whatever unit is in the queue by queuing an item we don't want to produce
} else if (myGold <= 90 && myWorkerCount < maxWorkers && base.shouldCancel) {
base.build(base.units.me.heavy);
base.shouldCancel = false;
// Additional safety.. should never reach here
} else if (myWorkerCount < maxWorkers && base.shouldCancel) {
base.build(base.units.me.soldier);
base.shouldCancel = false;
}
// ------------------------------------------------------------
// Functions for working with bounding boxes
// ------------------------------------------------------------
// Convert a positions real location into its a square the size of the unit collection bounding box
var nearestEdge = function(a, b) {
return new Vector(
b.x + max(min(a.x - b.x, 2.95) , -2.95),
b.y + max(min(a.y - b.y, 3.95) , -3.95)
);
},
shortestLink = function(a, b) {
var diff = new Vector.subtract(nearestEdge(a, b), a);
diff.x = min(max(diff.x, -2.95), 2.95);
diff.y = min(max(diff.y, -3.95), 3.95);
diff = new Vector.limit(diff, 25);
return Vector.add(a, diff);
},
longestLink = function(a, b) {
var diff = new Vector.subtract(b, a);
return Vector.add(a, Vector.limit(diff, 5));
},
// Add a to a vector array to improve distance calculation performance
addCoinToBoundedBox = function(coin) {
var coinId = coin.id;
if (!hybridCoinMap[coinId]) {
// Track coin amounts
base.coins++;
base.totalCoins++;
base.maxCoins = max(base.coins, base.maxCoins);
var coinPos = coin.pos,
newCoin = new Vector(coinPos.x, coinPos.y),
bucketX = floor(coinPos.x / sizeX),
bucketY = floor(coinPos.y / sizeY);
newCoin.magnitude = coinId;
newCoin.equals = coin.bountyGold * 1000;
// Add coin to list of hybrid coins, and keep track of which coin ids map to the same hybrid coin
hybridCoins.push(newCoin);
// HybridCoinMap maps all coin ids to their hybrid coin objects
hybridCoinMap[coinId] = coin;
// Manage local centroid
if ( !centroids[bucketX] )
centroids[bucketX] = {};
var bucket = centroids[bucketX][bucketY];
if ( bucket ) {
bucket.normalize = Vector.add(bucket.normalize, coinPos);
var localCentroid = Vector.divide(bucket.normalize, ++bucket.dot);
//bucket.x = localCentroid.x;
//bucket.y = localCentroid.y;
bucket.equals += newCoin.equals;
++bucket.heading;
} else {
var copy = new Vector(bucketX * 26.333 + 26.333 / 2 + 3, bucketY * 31 + 31 / 2 + 4);
copy.dot = copy.heading = 1;
copy.normalize = copy.copy();
copy.equals = newCoin.equals;
centroids[bucketX][bucketY] = copy;
}
}
},
// Remove a single coin from a bounding box. This might decrease the value of the box, or it could remove the box all together
removeCoinFromBoundedBox = function(coin, coinsRemoved) {
var coinId = coin.id;
if (hybridCoinMap[coinId]) {
var hybridIndex = indexOf(prevItems, coin) - coinsRemoved,
coinPos = coin.pos,
bucketX = floor(coinPos.x / sizeX),
bucketY = floor(coinPos.y / sizeY);
var centroidCoin = centroids[bucketX][bucketY];
// Decrease the value of the centroid this coin belonged to
centroidCoin.equals -= (coin.bountyGold * 1000);
--centroidCoin.heading;
base.coins--;
// Remove the collected coin if we havent already
if (hybridIndex !== -1) {
hybridCoins.splice(hybridIndex, 1);
}
delete hybridCoinMap[coinId];
}
},
wd,
// Low op variable store
badChoice = function(worker, coin) {
return ((wd = worker.pos.distance(coin)) && myWorkerVectors.some(function(myWorker) {
return (myWorker.distance(coin) || inf) < wd;
}));
},
// Find approximate sets of clusters nearest to a worker
findNearestClusters = function(worker) {
var clusterPoolLength = 14,
anchorNodes = 3,
pathLength = 2,
clusterPool = [],
nearestCoins = [],
paths = [],
keys = [],
idealCoins = 0,
coins = hybridCoins.slice(0);
// Get the nearest k coins to a worker - all clusters will also be made from these coins
clusterPoolLength = min(base.coins, clusterPoolLength);
while (clusterPoolLength--) {
var nearest = worker.getNearest(coins.slice(0));
// Add the coin to the pool if its not a bad choice... or if theres no better choices
if ( !(coinsToAvoid[nearest.magnitude] || badChoice(worker, nearest)) ) {
clusterPool.push(nearest);
// Once we have 10 ideal coins, we can stop looking for more
if (++idealCoins > 10)
break;
}
// Replace coin so that it wont get selected again - can't splice because we lose index relationship after a coin is removed
coins[indexOf(currentItems, hybridCoinMap[nearest.magnitude])] = distantCoin;
}
// Limit nearest coins to the number of paths we want to return
nearestCoins = clusterPool.slice(0, anchorNodes);
// Locate the nearest cluster of coins from each anchor that we want to test
nearestCoins.forEach(function(anchor, index) {
var path = [],
sorted = [],
result = [],
resultKeys = [],
pool = clusterPool.slice(0);
// Remove self from pool
pool.splice(index, 1);
// Find the distance from the anchor node to all other nodes
pool.forEach(function(coin) {
// Add distance to array for us to sort to find closest
path.push(anchor.distance(coin) / coin.equals);
});
// Sort the distance array to locate closest coins
sorted = path.slice(0);
sorted.sort();
// Take the first k elements of the sorted array, and find out which coin they were
sorted.slice(0, pathLength).forEach(function(dist) {
var coin = pool[indexOf(path, dist)];
result.push(coin);
resultKeys.push(coin.magnitude);
});
// Store result in the index of the anchor coin
paths.push(result);
keys.push(resultKeys);
});
// Return nearest coins and their corresponding clusters
return [nearestCoins, paths, keys];
},
kNearestCoins = function(worker, k) {
var nearestCoins = [],
coins = hybridCoins.slice(0);
// Get the nearest k coins to a worker - all clusters will also be made from these coins
k = min(base.coins, k);
while (k--) {
var nearest = worker.getNearest(coins.slice(0));
nearestCoins.push(nearest);
// Replace coin so that it wont get selected again
coins[indexOf(currentItems, hybridCoinMap[nearest.magnitude])] = distantCoin;
}
return nearestCoins;
},
// Methods for Priority Queue
nodes = [];
// ------------------------------------------------------------
// Track Workers as pure vectors
// ------------------------------------------------------------
if (lateGame) {
myWorkers.forEach(function(worker) {
workerDist[worker.id] = [];
workerCoins[worker.id] = [];
myWorkerVectors.push(worker.pos);
});
enemyWorkers.forEach(function(worker) {
workerDist[worker.id] = [];
workerCoins[worker.id] = [];
enemyWorkerVectors.push(worker.pos);
});
} else {
myWorkers.forEach(function(worker) {
myWorkerVectors.push(worker.pos);
});
enemyWorkers.forEach(function(worker) {
enemyWorkerVectors.push(worker.pos);
});
}
// ------------------------------------------------------------
// Track Newly Added/Removed coins
// ------------------------------------------------------------
// Total amount of coins collected this turn
var coinsRemoved = 0;
// Find coins that were just collected
prevItems.forEach(function(element) {
// Coins collected this turn
if (indexOf(currentItems, element) === -1) {
removeCoinFromBoundedBox(element, coinsRemoved++);
}
});
// The amount of new coins that need to be added
var newCoins = (currentItemCount - prevItemCount) + coinsRemoved;
// If theres new coins, add them to the hybrid list
if (newCoins > 0) {
// The index in currentItem array of the current new coin we need to add
var newCoinIndex = currentItemCount + (prevItemCount - currentItemCount) - coinsRemoved;
// Add all the new coins
while (newCoins--) {
addCoinToBoundedBox(currentItems[newCoinIndex++]);
}
}
// Rebuild centroid list ... array of objects seems to desync
if (newCoins || coinsRemoved) {
centroidList = [];
for (var bucketX in centroids)
for (var bucketY in centroids[bucketX])
centroidList.push(centroids[bucketX][bucketY]);
}
// Force changes to persist - seems to be a bug
base.hybridCoins = hybridCoins.slice(0);
// ------------------------------------------------------------
// Simple pathing
// ------------------------------------------------------------
// Steal enemy coins - don't be fooled by scaled vectors
var arcDistance = 7.85398, // 5 * pi / 2;
commandedWorkers = {},
workersToRemove = [];
if (now <= 80)
enemyWorkers.forEach(function(element, i) {
var targetPos = element.targetPos || false;
if (targetPos) {
// Figure out the distance and direction the enemy is traveling
var nearestCoins = kNearestCoins(element, 10),
elementPos = element.pos,
direction = new Vector.subtract(targetPos, elementPos),
heading = direction.heading(),
workerIndex = -1,
toAvoid = [],
toAvoidCoins = [],
next = [],
nextCoin = [],
realTarget, coinTarget, closestCoin, distance, enemyDistance;
// Find the first coin that will intersect with the workers path
nearestCoins.some(function(coin, index) {
// Find out the distance and direction a coin is from an enemy
var dir = Vector.subtract(coin, elementPos),
dist = dir.magnitude();
// If coin is on the current worker path
if (abs(heading - dir.heading()) <= (arcDistance / dist)) {
// The closest coin on route is the real target of the enemy
if ( !coinTarget ) {
distance = enemyDistance = dist;
coinTarget = coin;
realTarget = Vector.add(elementPos, direction.limit(dist));
nearestCoins.splice(index, 1);
return true;
}
}
});
// If we have found a target coin for the enemy
if (distance) {
var minDist = ceil((distance - max(5, distance)) / 1.25) * 1.25,
newTarget = new Vector.add(elementPos, direction.limit(minDist));
// If the enemy is closer to these coins than we are avoid them for future targeting
// The enemy distance is really distance to current target + distance from target - coin
// Since we know where he's really headed...
nearestCoins.slice(0).forEach(function(coin) {
if (myWorkerVectors.every(function(worker) {
return (minDist + newTarget.distance(coin)) < worker.distance(coin);
})) {
if ( !coinsToAvoid[coin.magnitude] ) {
// Doesn't really matter what we push as long as its unique enough to match back
toAvoid.push(coinTarget.distance(coin) / coin.equals);
toAvoidCoins.push(coin);
}
} else {
next.push(coinTarget.distance(coin) / 1000);
nextCoin.push(coin);
}
});
if ( next.length ) {
var sortedNext = next.slice(0);
sortedNext.sort();
closestCoin = nextCoin[indexOf(next, sortedNext.shift())];
}
} else {
// Enemy doesn't really have a target... so trace distance from worker to 3 nearest coins
nearestCoins.slice(0, 3).forEach(function(coin) {
if (myWorkerVectors.every(function(worker) {
return (elementPos.distance(coin)) < worker.distance(coin);
})) {
if ( !coinsToAvoid[coin.magnitude] ) {
toAvoid.push(elementPos.distance(coin) / coin.equals);
toAvoidCoins.push(coin);
}
}
});
}
// Sort distances in avoidance list
var sortedAvoidDist = toAvoid.slice(0);
sortedAvoidDist.sort();
// Avoid the closest 2 coins, closest coin will also be avoided... so in total up to 3 coins avoided for each enemy
sortedAvoidDist.slice(2).forEach(function(dist) {
var coin = toAvoidCoins[indexOf(toAvoid, dist)];
coinsToAvoid[coin.magnitude] = coin;
});
// If we managed to tie a real coin to the enemies position, check if we can intercept
if (realTarget) {
// Find the distance the enemy is from his intended target
coinsToAvoid[coinTarget.magnitude] = coinTarget;
// Check to see if any of my workers can steal
myWorkerVectors.forEach(function(worker, index) {
var workerDist = worker.distance(coinTarget);
// If i can beat the enemy to wherever hes headed, I'm going to!
// Keep track of distances to locations a coin is already headed to, so we always make the best choice
if (workerDist < distance &&
(!commandedWorkers[index] || workerDist < commandedWorkers[index])) {
workerIndex = index;
distance = workerDist;
commandedWorkers[index] = workerDist;
}
});
// We found a worker that is closer to the coin than the enemy - steal his coin
if (workerIndex != -1) {
var myWorker = myWorkers[workerIndex],
enemyWorkerVectorsClone = enemyWorkerVectors.slice(0);
// Ensure that there isn't an enemy worker closer... sometimes enemies target the same coin
// Lets not follow their mistake
enemyWorkerVectorsClone.splice(i, 1);
// The enemy that we can beat to this coin isn't even the closest enemy... abort
if (enemyWorkerVectorsClone.slice(0).some(function(enemyWorker) {
return enemyWorker.distance(coinTarget) < distance;
})) {
commandedWorkers[workerIndex] = inf;
return;
}
// At this point we know that this worker is for sure the closest to the coin the enemy wants
// See if we can scoop up some coins along the way to the enmies position as well
var alliedNearestCoins = kNearestCoins(myWorker, 5),
closestMidCoin, midCoinValue = 0;
enemyDistance = ceil(enemyDistance / 1.25) * 1.25;
alliedNearestCoins.forEach(function(coin) {
// Find the best path from this coin to the coinTarget
var inBetweenTarget = shortestLink(coin, coinTarget),
myDist = myWorker.distance(inBetweenTarget),
normalDistance = ceil(myDist / 1.25) * 1.25,
dirVector = Vector.subtract(inBetweenTarget, coinTarget);
inBetweenPos = Vector.add(myWorker.pos, Vector.limit(dirVector, normalDistance));
// Now get distance from this intermediate coin to the target coin...
var endDistance = ceil(inBetweenPos.distance(coinTarget) / 1.25) * 1.25;
if ( (normalDistance + endDistance) < enemyDistance ) {
enemyDistance = normalDistance + endDistance;
closestMidCoin = coin;
}
});
// This command may or may not stick... it could be over written by a later loop
// If we find a better coin for this worker to steal
workersToRemove.push(myWorker.id);
if ( closestMidCoin) {
coinTarget = shortestLink(closestMidCoin, coinTarget);
} else if ( closestCoin )
coinTarget = shortestLink(coinTarget, closestCoin);
else
coinTarget = shortestLink(coinTarget, realTarget);
base.command(myWorker, 'move', coinTarget);
activeWorkers[myWorker.id] = coinTarget;
}
}
}
});
var centroidsClone = centroidList.slice(0),
workersLeft = [];
// These workers are stealing coins... remove them from the worker arrays since their moves are already decided
myWorkers.slice(0).forEach(!lateGame? function(element) {
if (workersToRemove.indexOf(element.id) == -1) {
var components = findNearestClusters(element),
minWeight = inf,
nextNode, nextNextNode, lastNode;
components[0].forEach(function(anchor, index) {
var anchorId = anchor.magnitude,
coins = components[1][index],
numCoins = coins.length,
nodesLeft = numCoins,
coinKeys = components[2][index],
clusterValue = 0,
distances = {};
// Add the anchor coin
nodes.push({ coin: anchor, distance: distances[anchorId] = 0 });
// Preset all distances to inf, and add vertices to queue
coins.forEach(function(coin, index) {
// Value of all coins in this set
clusterValue += coin.equals;
// Add node to
nodes.push({ coin: coin, distance: distances[coinKeys[index]] = inf });
});
++nodesLeft;
// Dijkstra - optimized for minimum ops - some fucked up algorithm changes
while ( nodesLeft-- ) {
var smallest = nodes.shift().coin,
smallestId = smallest.magnitude,
smallestDist = distances[smallestId],
coinCount = numCoins;
while (coinCount--) {
var coin = coins[coinCount],
coinId = coin.magnitude,
newDistance = smallestDist + ceil((coin.distance(smallest) || inf) / 1.25);
if (newDistance < distances[coinId]) {
nodes.push({ coin: coin, distance: distances[coinId] = newDistance });
++nodesLeft;
}
}
}
var maxDist = 0;
// Find the furthest distance to a node - this is the shortest path to all nodes
for (var coinDist in distances) {
if (distances[coinDist] > maxDist) {
maxDist = distances[coinDist];
lastNode = hybridCoinMap[coinDist].pos;
}
}
// The distance-per-coin metric
// We want to minimize this value, as the less distance we have to travel for each "value" the better
var pathValue = (element.pos.distance(anchor) / anchor.equals) + (maxDist / clusterValue);
// Check if this is the optimal path so far
if (pathValue < minWeight) {
minWeight = pathValue;
nextNode = anchor;
var minDist = inf;
// Find the coin closest to the anchor coin
for (coinDist in distances) {
if (distances[coinDist] < minDist && distances[coinDist] !== 0) {
minDist = distances[coinDist];
nextNextNode = coinDist;
}
}
if (nextNextNode)
nextNextNode = hybridCoinMap[nextNextNode].pos;
}
});
// If we have a good candidate node to travel to, make our way there
if (nextNode) {
// If theres at least 2 nodes to travel to, optimize path so that we minimize the distance to the next 2 nodes
if (nextNextNode) {
if (lastNode && lastNode !== nextNextNode)
nextNextNode = shortestLink(nextNextNode, lastNode);
nextNode = shortestLink(nextNode, nextNextNode);
}
base.command(element, 'move', nextNode);
activeWorkers[element.id] = nextNode;
} else {
// Further scheduling for these guys
workersLeft.push(element);
}
}
} : (endGame? function(element) {
//base.command(element, 'move', new Vector(80, 65));
} : function(element) {
// Just get a list of workers remaining
if (workersToRemove.indexOf(element.id) == -1)
workersLeft.push(element);
}));
// Calculations are expensive if theres a lot of coins... so only do it when there should be few
if (lateGame) {
// Find the closest coins to each worker
hybridCoins.forEach(function(element) {
var minDist = inf, minWorker;
myWorkers.forEach(function(worker) {
var pos = worker.pos;
if (minDist > pos.distance(element)) {
minDist = pos.distance(element);
minWorker = worker;
}
});
// Give control of the coin to the closest worker
workerDist[minWorker.id].push(minDist / element.equals);
workerCoins[minWorker.id].push(element);
});
// Try to isolate enemies so that if he's moving in a direction away from the centroid, we block him out from moving
// back towards it
var centroidsToRemove = [],
removed = 0;
// We can reasonably expect that all centroids exist by now - find out where enemies are
enemyWorkers.forEach(function(worker) {
var vector = worker.pos,
target = worker.targetPos;
// Figure out where this worker will be in 1 turn
if (target) {
var heading = new Vector.limit(Vector.subtract(target, vector), 1.25);
vector = new Vector.add(vector, heading);
}
var bucketX = floor(vector.x / sizeX),
bucketY = floor(vector.y / sizeY),
index = (bucketX * bucketCount) + bucketY;
if ( !enemyCentroids[index] )
enemyCentroids[index] = [];
enemyCentroids[index].push(vector);
});
// Find the average position of enemy workers in each box
for (var enemyList in enemyCentroids) {
var enemyCentroid = new Vector(0, 0),
list = enemyCentroids[enemyList],
length = list.length;
while (length--) {
enemyCentroid = Vector.add(enemyCentroid, list[length]);
}
enemyCentroids[enemyList] = Vector.divide(enemyCentroid, list.length);
}
// For all of our workers who have real destinations, find out what centroid they're headed to
// These centroids will become off limits to workers who have nothing to do
// (we know they can't reach coins inside them anyways)
for (var worker in activeWorkers) {
var vector = activeWorkers[worker],
index;
bucketX = floor(vector.x / sizeX);
bucketY = floor(vector.y / sizeY);
index = (bucketX * bucketCount) + bucketY;
centroidsToRemove.push(index / 100);
}
// Sort keys so we can remove centroids in order
centroidsToRemove.sort();
centroidsToRemove.forEach(function(index) {
var key = floor(index * 100);
centroidsClone.splice(key - removed++, 1);
});
}
// Move the worker to the closest coin
workersLeft.forEach(function(worker) {
var workerId = worker.id,
sortedDist = (workerDist[workerId] || []).slice(0),
length = sortedDist.length,
nearestCentroid = worker.getNearest(centroidsClone.slice(0)),
midVector = new Vector(42,35);
// Find the nearest centroid
centroidsClone.some(function(centroid, index) {
if (centroid == nearestCentroid) {
centroidsClone.splice(index, 1);
return true;
}
});
sortedDist.sort();
if ( !nearestCentroid )
nearestCentroid = midVector;
// Move to the nearest centroid
if ( !length ) {
var centroidIndex = floor(nearestCentroid.x / sizeX) * bucketCount + floor(nearestCentroid.y / sizeY);
// An enemy is in this square with me... try to zone him out if I can't beat him to coins
if ( enemyCentroids[centroidIndex] && nearestCentroid !== midVector ) {
base.command(worker, 'move', shortestLink(enemyCentroids[centroidIndex], nearestCentroid));
} else {
base.command(worker, 'move', nearestCentroid);
}
// Move to the nearest coin with best path back to centroid
} else {
var coin = workerCoins[workerId][indexOf(workerDist[workerId], sortedDist[0])];
// Shortest path to coin & back to centroid
if ( length == 1 ) {
base.command(worker, 'move', shortestLink(coin, nearestCentroid));
// Shortest path to next 2 coins
} else {
var secondCoin = workerCoins[workerId][indexOf(workerDist[workerId], sortedDist[1])];
base.command(worker, 'move', shortestLink(coin, secondCoin));
}
}
});
}
// How many frames have we dropped up until this point?
base.droppedFrames = (now * base.frameRate) - ( base.frames++ );
base.lastHealth = base.health;
base.lastArmySize = myArmySize;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment