1// Warlord — aggressive reference bot for Tribes v2.2// @description: Raiders. Weapons-first smithy, early muster, drilled warbands that loot the fat and besiege the weak.3//4// Strategy: the economy exists to feed the smithy and the smithy exists to5// arm warbands. Lean extraction, weapons-first craft split, early muster,6// then raid the nearest enemy settlement — escalating to a siege once a band7// is heavy enough. Targeting is event-driven: 'sighted' and 'battle' events8// plus live enemySettlements sightings pick the victim; with no target the9// bands patrol seeded-random waypoints (api.rng — deterministic per seed).10//11// Contract: define a global `function tick(state, memory, api)`.1213function tick(state, memory, api) {14 // Prestige: try to advance the age when the hoard allows (the engine15 // drops the order if we cannot afford it — a cheap periodic attempt).16 if (state.tick % 50 === 0 && state.age < 3) api.advance();1718 var home = state.settlements[0];19 if (!home) return;2021 // Herd management (hysteresis): slaughter only while the flock is fat —22 // hides feed leather and the age hoard — and let a thin flock recover.23 var herds = home.herds || 0;24 if (!memory.slaughterOn && herds >= 16) {25 api.decree("slaughter", 80);26 memory.slaughterOn = 1;27 } else if (memory.slaughterOn && herds < 8) {28 api.decree("slaughter", 0);29 memory.slaughterOn = 0;30 }3132 function hasOrQueued(building) {33 if (home.buildings[building]) return true;34 for (var i = 0; i < home.buildQueue.length; i++) {35 if (home.buildQueue[i].building === building) return true;36 }37 return false;38 }3940 function manhattan(x1, y1, x2, y2) {41 var dx = x1 > x2 ? x1 - x2 : x2 - x1;42 var dy = y1 > y2 ? y1 - y2 : y2 - y1;43 return dx + dy;44 }4546 // Standing + queued count of a building at home.47 function countOf(building) {48 var n = home.buildings[building] || 0;49 for (var ci = 0; ci < home.buildQueue.length; ci++) {50 if (home.buildQueue[ci].building === building) n++;51 }52 return n;53 }5455 // ------------------------------------------------------------------56 // One-time policy init (persistent). Enough food to grow, ore/metal57 // heavy, weapons-first smithy. Sums to 940 permille.58 // ------------------------------------------------------------------59 if (!memory.init) {60 memory.init = 1;61 api.allocate("farmer", 310);62 api.allocate("fisher", 80);63 api.allocate("woodcutter", 160); // timber first: no camp, no smithy, no war64 api.allocate("miner", 60); // ore for the smelter (once a mine exists)65 api.allocate("miller", 40);66 api.allocate("baker", 60);67 api.allocate("smelter", 80);68 api.allocate("smith", 70); // the point of everything69 api.allocate("builder", 90);70 api.allocate("scholar", 40); // research gates the war chain71 api.allocate("hauler", 30);72 // Weapons before comfort; just enough tools to keep the mines fed.73 api.craft("tools", 250);74 api.craft("weapons", 600);75 api.craft("armor", 150);76 api.craft("mill", 800);77 api.craft("brew", 0);78 api.craft("fodder", 200); // horses later, maybe79 api.allocate("herder", 30);80 api.decree("rations", 1000);81 api.decree("expansion", 1000); // all ground within reach is ours // hungry soldiers desert — feed the war machine82 api.say("warlord: sharpen everything");83 return;84 }8586 // ------------------------------------------------------------------87 // Research: smelting -> weaponsmithing -> siegecraft. Metal, then88 // better weapons, then the means to crack walls.89 // ------------------------------------------------------------------90 if (state.researching === null) {91 if (state.techs.indexOf("smelting") < 0) api.research("smelting");92 else if (state.techs.indexOf("weaponsmithing") < 0) api.research("weaponsmithing");93 else if (state.techs.indexOf("siegecraft") < 0) api.research("siegecraft");94 }9596 // ------------------------------------------------------------------97 // Construction: the war chain only. Smithy above all.98 // ------------------------------------------------------------------99 if (home.buildQueue.length === 0) {100 // Growth targets scale with population: more mouths need more farms,101 // more people need more roofs. countOf() checks standing + queued.102 var wantFarms = 1 + Math.floor(home.pop / 60);103 var wantHouses = Math.floor(home.pop / 45);104 var wantCamps = 1 + Math.floor(home.pop / 150);105 if (countOf("farm") < wantFarms) api.build("farm", home.id);106 else if (countOf("lumber_camp") < wantCamps) api.build("lumber_camp", home.id);107 else if (countOf("pasture") < 1 + Math.floor(home.pop / 500)) api.build("pasture", home.id);108 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);109 else if (countOf("house") < wantHouses) api.build("house", home.id);110 }111 // ------------------------------------------------------------------112 // Ore or death: if our claims hold no hills (a neighbor beat us to113 // them), FOUND a mining outpost by the nearest highland and route its114 // ore home — the war chain cannot exist without metal.115 // ------------------------------------------------------------------116 if (117 state.settlements.length < 2 &&118 home.pop >= 500 &&119 (memory.foundTry === undefined || state.tick - memory.foundTry > 100)120 ) {121 var best = null;122 var bestD = -1;123 var size = state.mapSize;124 for (var ti = 0; ti < state.terrain.length; ti++) {125 // unclaimed explored highland, or a distant highland silhouette ('^')126 var isHill = false;127 var ch = state.terrain[ti];128 if ((ch === "h" || ch === "O" || ch === "m") && state.claims[ti] === ".") isHill = true;129 else if (state.distant[ti] === "^") isHill = true;130 if (!isHill) continue;131 var tx = ti % size;132 var ty = (ti / size) | 0;133 // the engine refuses camps within 6 of ANY settlement — keep margin134 var tooClose = false;135 if (Math.max(Math.abs(tx - home.x), Math.abs(ty - home.y)) < 6) tooClose = true;136 for (var es = 0; es < state.enemySettlements.length && !tooClose; es++) {137 var e = state.enemySettlements[es];138 if (Math.max(Math.abs(tx - e.x), Math.abs(ty - e.y)) < 6) tooClose = true;139 }140 if (tooClose) continue;141 var d = Math.max(Math.abs(tx - home.x), Math.abs(ty - home.y));142 if (best === null || d < bestD) { best = { x: tx, y: ty }; bestD = d; }143 }144 if (best !== null) {145 // camp on the nearest open ground beside the highland146 var cx = best.x;147 var cy = best.y;148 for (var r = 1; r <= 3 && state.terrain[cy * size + cx] !== "." ; r++) {149 for (var dy = -r; dy <= r; dy++) {150 for (var dx = -r; dx <= r; dx++) {151 var nx = best.x + dx;152 var ny = best.y + dy;153 if (nx < 0 || ny < 0 || nx >= size || ny >= size) continue;154 var nch = state.terrain[ny * size + nx];155 if (nch === "." || nch === "t") { cx = nx; cy = ny; dy = r + 1; break; }156 }157 }158 }159 // only pack what the stores can actually spare — unaffordable supplies void the order160 var pack = {};161 var wagon = { grain: 150, timber: 80, stone: 20, tools: 8 };162 for (var g in wagon) {163 var have = home.stores[g] || 0;164 if (have > wagon[g] * 2) pack[g] = wagon[g];165 else if (have > 20) pack[g] = (have / 2) | 0;166 }167 api.found(cx, cy, 80, pack);168 memory.foundTry = state.tick; // retry every 100t until the camp exists169 if (!memory.saidHills) {170 memory.saidHills = 1;171 api.say("warlord: the hills will be ours");172 }173 }174 }175 // Mines wherever hills are held; ore flows home for the smelter.176 if (state.tick % 40 === 0) {177 for (var si = 0; si < state.settlements.length; si++) {178 var stl = state.settlements[si];179 if (!stl.buildings.mine && stl.buildQueue.length < 2) api.build("mine", stl.id);180 if (stl.id !== home.id && !memory["route" + stl.id]) {181 api.route(stl.id, home.id, "ore", 700);182 api.route(stl.id, home.id, "stone", 600);183 memory["route" + stl.id] = 1;184 }185 }186 }187188 // The strategic plan advances IN PARALLEL with growth (a short queue always189 // has room for one plan item, or growth would crowd it out forever).190 if (home.buildQueue.length < 2) {191 var plan = ["library", "mine", "smelter", "smithy", "barracks", "granary"];192 for (var p = 0; p < plan.length; p++) {193 if (!hasOrQueued(plan[p])) {194 api.build(plan[p], home.id);195 break;196 }197 }198 }199200 // ------------------------------------------------------------------201 // Target selection, event-driven:202 // 1. Live sighting of an enemy settlement -> nearest one is the target.203 // 2. A battle event or enemy 'sighted' event -> remember the location204 // as a hunting ground to patrol (sightings decay after 60 ticks).205 // ------------------------------------------------------------------206 var target = null; // { id, x, y } enemy settlement207 var bestD = -1;208 for (var s = 0; s < state.enemySettlements.length; s++) {209 var es = state.enemySettlements[s];210 var d = manhattan(home.x, home.y, es.x, es.y);211 if (target === null || d < bestD) {212 target = es;213 bestD = d;214 }215 }216 if (target !== null) {217 // Refresh the grudge and make the hostility formal (persistent stance).218 if (memory.foe !== target.tribe) {219 memory.foe = target.tribe;220 api.stance(target.tribe, "hostile");221 api.log("warlord: marked tribe " + target.tribe + " hostile");222 }223 memory.huntX = target.x;224 memory.huntY = target.y;225 memory.huntUntil = state.tick + 60;226 } else {227 for (var e = 0; e < state.events.length; e++) {228 var ev = state.events[e];229 if (ev.t === "sighted" && ev.tribe === state.tribeId) {230 memory.huntX = ev.x;231 memory.huntY = ev.y;232 memory.huntUntil = state.tick + 60;233 } else if (ev.t === "battle" && ev.attacker !== state.tribeId) {234 memory.huntX = ev.x;235 memory.huntY = ev.y;236 memory.huntUntil = state.tick + 60;237 }238 }239 }240241 // ------------------------------------------------------------------242 // Muster: raise infantry bands (up to 3) sized to whatever the armory243 // can actually equip (starter stock is small; the smithy chain grows it).244 // Bands drill after mustering, so raise them EARLY and let them train245 // while the economy builds. Tick-stamp guard against re-muster spam.246 // ------------------------------------------------------------------247 var armory = home.stores.weapons || 0;248 var bandSize = armory >= 30 ? 30 : armory;249 if (250 state.warbands.length < 3 &&251 bandSize >= 15 &&252 (memory.musterAt === undefined || state.tick - memory.musterAt > 10)253 ) {254 api.muster(home.id, bandSize, "infantry");255 memory.musterAt = state.tick;256 }257258 // ------------------------------------------------------------------259 // Command loop. With a live settlement target: light bands raid it260 // (loot, no long commitment), heavy bands (50+) settle in to besiege.261 // No target: patrol the last hunting ground, else a seeded-random262 // waypoint re-rolled every 40 ticks.263 // ------------------------------------------------------------------264 for (var w = 0; w < state.warbands.length; w++) {265 var band = state.warbands[w];266 if (band.drilling > 0) {267 // Green troops train at home before campaigning (raid/besiege would268 // be refused anyway; don't waste the order budget).269 if (band.order.type !== "garrison") {270 api.command(band.id, { type: "garrison", target: band.home });271 }272 } else if (target !== null) {273 var wanted = band.size >= 50 ? "besiege" : "raid";274 if (band.order.type !== wanted || band.order.target !== target.id) {275 api.command(band.id, { type: wanted, target: target.id });276 }277 } else if (memory.huntUntil !== undefined && state.tick < memory.huntUntil) {278 if (band.order.type !== "patrol") {279 api.command(band.id, { type: "patrol", target: { x: memory.huntX, y: memory.huntY } });280 }281 } else {282 if (memory.scoutUntil === undefined || state.tick >= memory.scoutUntil) {283 memory.scoutX = Math.floor(api.rng() * state.mapSize);284 memory.scoutY = Math.floor(api.rng() * state.mapSize);285 memory.scoutUntil = state.tick + 40;286 }287 var pt = band.order.target;288 var onWaypoint =289 band.order.type === "patrol" &&290 pt !== null &&291 typeof pt === "object" &&292 pt.x === memory.scoutX &&293 pt.y === memory.scoutY;294 if (!onWaypoint) {295 api.command(band.id, { type: "patrol", target: { x: memory.scoutX, y: memory.scoutY } });296 }297 }298 }299}