clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

main
tribes / packages / script-host / src / bots.ts
bots.ts 44.4K
1// GENERATED FILE — do not edit by hand.2// Source of truth: ../bots/*.js. Regenerate with: node scripts/gen-bots.mjs34export const homesteaderSource = `// Homesteader — balanced-economy reference bot for Tribes v2.5// @description: Balanced farm-first economy: granaries, tools before weapons, a modest garrison behind level-1 walls.6//7// Strategy: a broad, self-sufficient economy with food security first. Set8// the whole labor/craft policy ONCE (policies persist — spending gas9// re-issuing them every tick is the classic v2 beginner mistake), then make10// small guarded adjustments: bump farmers when the larder runs thin, queue11// one building at a time, keep exactly one garrison warband once the smithy12// has produced weapons. Tech: milling -> crop_rotation (the food multipliers).13//14// Contract: define a global \`function tick(state, memory, api)\`.15// state - TribeView JSON snapshot (fog-of-war filtered), fresh each tick16// memory - plain object persisted between ticks (JSON size capped)17// api - policy verbs + api.rng() (seeded stream) + api.log(msg)1819function tick(state, memory, api) {20 // Prestige: try to advance the age when the hoard allows (the engine21 // drops the order if we cannot afford it — a cheap periodic attempt).22 if (state.tick % 50 === 0 && state.age < 3) api.advance();2324 var home = state.settlements[0];25 if (!home) return; // eliminated-in-all-but-name; nothing to govern2627 // Herd management (hysteresis): slaughter only while the flock is fat —28 // hides feed leather and the age hoard — and let a thin flock recover.29 var herds = home.herds || 0;30 if (!memory.slaughterOn && herds >= 16) {31 api.decree("slaughter", 80);32 memory.slaughterOn = 1;33 } else if (memory.slaughterOn && herds < 8) {34 api.decree("slaughter", 0);35 memory.slaughterOn = 0;36 }3738 // True when \`building\` is already standing or in the build queue at home.39 function hasOrQueued(building) {40 if (home.buildings[building]) return true;41 for (var i = 0; i < home.buildQueue.length; i++) {42 if (home.buildQueue[i].building === building) return true;43 }44 return false;45 }4647 // Standing + queued count of a building at home.48 function countOf(building) {49 var n = home.buildings[building] || 0;50 for (var ci = 0; ci < home.buildQueue.length; ci++) {51 if (home.buildQueue[ci].building === building) n++;52 }53 return n;54 }5556 // ------------------------------------------------------------------57 // One-time policy init. Everything here is PERSISTENT policy: issued58 // once, in force until changed. Sums to 930 permille; the idle59 // remainder feeds population growth.60 // ------------------------------------------------------------------61 if (!memory.init) {62 memory.init = 1;63 // Extraction (tools-hungry, the base of every chain).64 api.allocate("farmer", 280);65 api.allocate("fisher", 90);66 api.allocate("woodcutter", 100);67 api.allocate("miner", 60);68 // Refinement + finished goods.69 api.allocate("miller", 50);70 api.allocate("baker", 70);71 api.allocate("brewer", 30);72 api.allocate("smelter", 40);73 api.allocate("smith", 50);74 api.allocate("carpenter", 30);75 // Infrastructure & services.76 api.allocate("builder", 80);77 api.allocate("hauler", 30);78 api.allocate("scholar", 20);79 // Smithy split: tools keep extraction efficient; a trickle of weapons80 // for the eventual garrison. Grain split: mostly milled for bread.81 api.craft("tools", 750);82 api.craft("weapons", 200);83 api.craft("armor", 50);84 api.craft("mill", 800);85 api.craft("brew", 100);86 api.craft("fodder", 100);87 api.allocate("herder", 30);88 api.decree("rations", 1000);89 api.decree("expansion", 1000); // every acre ploughed: the homestead sprawls // full rations: morale + growth90 api.say("homesteader: tilling the good earth");91 return; // first tick done — keep it cheap and under the action cap92 }9394 // ------------------------------------------------------------------95 // Research: milling then crop_rotation (both multiply the food economy).96 // Re-issuing while idle is safe; the engine drops what it can't start.97 // ------------------------------------------------------------------98 if (state.researching === null) {99 if (state.techs.indexOf("milling") < 0) api.research("milling");100 else if (state.techs.indexOf("crop_rotation") < 0) api.research("crop_rotation");101 }102103 // ------------------------------------------------------------------104 // Food security: if the larder holds less than one unit of food-ish105 // stock per citizen, bump farmers (a guarded ADJUSTMENT — only when the106 // current policy is actually below the target, so this costs gas rarely).107 // ------------------------------------------------------------------108 var larder =109 (home.stores.bread || 0) + (home.stores.grain || 0) + (home.stores.fish || 0);110 if (larder < home.pop && state.allocations.farmer < 360) {111 api.allocate("farmer", 360);112 api.log("homesteader: larder thin (" + larder + "), more farmers");113 }114115 // ------------------------------------------------------------------116 // Construction: one queued building at a time, first missing item of a117 // fixed priority list. Farm/mill/bakery power the bread chain, granary118 // protects the harvest, smithy makes tools/weapons, walls L1 for safety.119 // ------------------------------------------------------------------120 if (home.buildQueue.length === 0) {121 // Growth targets scale with population: more mouths need more farms,122 // more people need more roofs. countOf() checks standing + queued.123 var wantFarms = 1 + Math.floor(home.pop / 60);124 var wantHouses = Math.floor(home.pop / 45);125 var wantCamps = 1 + Math.floor(home.pop / 150);126 if (countOf("farm") < wantFarms) api.build("farm", home.id);127 else if (countOf("lumber_camp") < wantCamps) api.build("lumber_camp", home.id);128 else if (countOf("pasture") < 1 + Math.floor(home.pop / 500)) api.build("pasture", home.id);129 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);130 else if (countOf("house") < wantHouses) api.build("house", home.id);131 }132 // The strategic plan advances IN PARALLEL with growth (a short queue always133 // has room for one plan item, or growth would crowd it out forever).134 if (home.buildQueue.length < 2) {135 var plan = ["granary", "mill", "bakery", "smithy", "pasture", "walls", "shrine", "library"];136 for (var p = 0; p < plan.length; p++) {137 if (!hasOrQueued(plan[p])) {138 api.build(plan[p], home.id);139 break;140 }141 }142 }143144 // ------------------------------------------------------------------145 // Minimal military: exactly one infantry band, garrisoned at home, once146 // the smithy has turned out enough weapons. The tick-stamp guard stops147 // us re-mustering every tick while the engine processes the first one.148 // ------------------------------------------------------------------149 if (150 state.warbands.length === 0 &&151 (home.stores.weapons || 0) >= 20 &&152 (memory.musterAt === undefined || state.tick - memory.musterAt > 10)153 ) {154 api.muster(home.id, 20, "infantry");155 memory.musterAt = state.tick;156 }157 for (var w = 0; w < state.warbands.length; w++) {158 var band = state.warbands[w];159 if (band.order.type !== "garrison") {160 api.command(band.id, { type: "garrison", target: home.id });161 }162 }163}164`;165166export const bulwarkSource = `// Bulwark — defensive reference bot for Tribes v2.167// @description: Fortress doctrine: stone walls, towers, armored garrisons, warding rites. Sells ale, buys metal, outlasts raiders.168//169// Strategy: dig in and outlast. Rush masonry, stack walls and towers, and170// keep a large armored garrison at home. Faith goes into warding rites when171// we are actually being hit. The economy leans on stone/ore extraction and172// pays for imported metal by selling surplus ale (a standing repeat offer).173// Never attacks; every warband holds the walls.174//175// Contract: define a global \`function tick(state, memory, api)\`.176177function tick(state, memory, api) {178 // Prestige: try to advance the age when the hoard allows (the engine179 // drops the order if we cannot afford it — a cheap periodic attempt).180 if (state.tick % 50 === 0 && state.age < 3) api.advance();181182 var home = state.settlements[0];183 if (!home) return;184185 // Herd management (hysteresis): slaughter only while the flock is fat —186 // hides feed leather and the age hoard — and let a thin flock recover.187 var herds = home.herds || 0;188 if (!memory.slaughterOn && herds >= 16) {189 api.decree("slaughter", 80);190 memory.slaughterOn = 1;191 } else if (memory.slaughterOn && herds < 8) {192 api.decree("slaughter", 0);193 memory.slaughterOn = 0;194 }195196 function hasOrQueued(building) {197 if (home.buildings[building]) return true;198 for (var i = 0; i < home.buildQueue.length; i++) {199 if (home.buildQueue[i].building === building) return true;200 }201 return false;202 }203204 // Did last tick's events show us taking hits?205 function underAttack() {206 for (var i = 0; i < state.events.length; i++) {207 var e = state.events[i];208 if (e.t === "battle" && e.defender === state.tribeId) return true;209 if (e.t === "raid" && e.tribe === state.tribeId) return true;210 if (e.t === "building_destroyed" && e.tribe === state.tribeId && e.by !== null) return true;211 }212 return false;213 }214215 // Standing + queued count of a building at home.216 function countOf(building) {217 var n = home.buildings[building] || 0;218 for (var ci = 0; ci < home.buildQueue.length; ci++) {219 if (home.buildQueue[ci].building === building) n++;220 }221 return n;222 }223224 // ------------------------------------------------------------------225 // Phased labor policy (persistent). Ten souls cannot run a fortress:226 // the founding camp farms and builds; the fortress diet — stone, ore,227 // steel — waits until there are backs enough to carry it.228 // ------------------------------------------------------------------229 if (!memory.phase) {230 memory.phase = 1;231 api.allocate("farmer", 620);232 api.allocate("woodcutter", 200);233 api.allocate("builder", 150); // roofs first, ramparts later234 api.craft("tools", 700);235 api.craft("weapons", 300);236 api.craft("mill", 800);237 api.craft("brew", 150);238 api.craft("fodder", 50);239 api.allocate("herder", 30);240 api.decree("rations", 1000);241 api.decree("expansion", 700); // walls first, acreage second242 api.say("bulwark: stone outlasts steel");243 return;244 }245 if (memory.phase === 1 && home.pop >= 150) {246 memory.phase = 2;247 api.allocate("farmer", 300);248 api.allocate("woodcutter", 90);249 api.allocate("miner", 150); // stone + ore: the fortress diet250 api.allocate("miller", 40);251 api.allocate("baker", 50);252 api.allocate("brewer", 60); // ale surplus is our trade good253 api.allocate("smelter", 60);254 api.allocate("smith", 60);255 api.allocate("builder", 90); // the wall never stops rising256 api.allocate("scholar", 40); // masonry does not research itself257 api.allocate("priest", 30); // faith for warding rites258 // Smithy split: garrison gets steel on its back, not just in hand.259 api.craft("tools", 400);260 api.craft("weapons", 300);261 api.craft("armor", 300);262 api.craft("mill", 700);263 api.craft("brew", 250);264 return;265 }266267 // ------------------------------------------------------------------268 // Research: masonry first (walls), then castellany (fortification),269 // then plate_armor for the garrison.270 // ------------------------------------------------------------------271 if (state.researching === null) {272 if (state.techs.indexOf("masonry") < 0) api.research("masonry");273 else if (state.techs.indexOf("castellany") < 0) api.research("castellany");274 else if (state.techs.indexOf("plate_armor") < 0) api.research("plate_armor");275 }276277 // ------------------------------------------------------------------278 // Construction: fortifications first, economy second. Walls stack (the279 // count is the wall level), so keep raising them to 3; towers to 4.280 // One queued job at a time so the builders never thrash.281 // ------------------------------------------------------------------282 if (home.buildQueue.length === 0) {283 // Growth targets scale with population: more mouths need more farms,284 // more people need more roofs. countOf() checks standing + queued.285 var wantFarms = 1 + Math.floor(home.pop / 60);286 var wantHouses = Math.floor(home.pop / 45);287 var wantCamps = 1 + Math.floor(home.pop / 150);288 if (countOf("farm") < wantFarms) api.build("farm", home.id);289 else if (countOf("lumber_camp") < wantCamps) api.build("lumber_camp", home.id);290 else if (countOf("pasture") < 1 + Math.floor(home.pop / 500)) api.build("pasture", home.id);291 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);292 else if (countOf("house") < wantHouses) api.build("house", home.id);293 else if (!hasOrQueued("granary")) api.build("granary", home.id);294 }295 // Fortifications advance IN PARALLEL with growth.296 if (home.buildQueue.length < 2) {297 var walls = home.buildings.walls || 0;298 var towers = home.buildings.tower || 0;299 var stone = home.stores.stone || 0;300 if (!hasOrQueued("library")) api.build("library", home.id);301 else if (!hasOrQueued("mine")) api.build("mine", home.id);302 // affordability guards: an unaffordable step must not stall the chain303 else if (!hasOrQueued("smelter") && stone >= 40) api.build("smelter", home.id);304 else if (!hasOrQueued("smithy") && stone >= 20) api.build("smithy", home.id);305 else if (walls < 1) api.build("walls", home.id);306 else if (!hasOrQueued("barracks")) api.build("barracks", home.id);307 else if (!hasOrQueued("brewery")) api.build("brewery", home.id);308 else if (walls < 3 && (home.stores.stone || 0) >= 80) api.build("walls", home.id);309 else if (towers < 4 && (home.stores.stone || 0) >= 60) api.build("tower", home.id);310 else if (!hasOrQueued("shrine")) api.build("shrine", home.id);311 }312313 // ------------------------------------------------------------------314 // Trade: one standing repeat offer — surplus ale for metal, open to any315 // non-hostile tribe. Only (re)posted when we hold no such offer.316 // ------------------------------------------------------------------317 var haveAleOffer = false;318 for (var t = 0; t < state.offers.length; t++) {319 var offer = state.offers[t];320 if (offer.tribe === state.tribeId && offer.give === "ale") haveAleOffer = true;321 }322 if (323 !haveAleOffer &&324 (home.stores.ale || 0) >= 40 &&325 (memory.offerAt === undefined || state.tick - memory.offerAt > 10)326 ) {327 api.trade({ give: "ale", giveAmount: 20, want: "metal", wantAmount: 8, tribe: null, repeat: true });328 memory.offerAt = state.tick; // let the engine register it before re-posting329 }330331 // ------------------------------------------------------------------332 // Garrison: up to two 30-strong infantry bands as weapons allow, all333 // parked on garrison duty at home. Tick-stamp guard against re-muster334 // spam while the engine raises the band.335 // ------------------------------------------------------------------336 var armory = home.stores.weapons || 0;337 if (338 state.warbands.length < 2 &&339 home.pop >= 120 &&340 armory >= 12 &&341 (memory.musterAt === undefined || state.tick - memory.musterAt > 10)342 ) {343 api.muster(home.id, Math.min(40, armory), "infantry");344 memory.musterAt = state.tick;345 }346 for (var w = 0; w < state.warbands.length; w++) {347 var band = state.warbands[w];348 if (band.order.type !== "garrison") {349 api.command(band.id, { type: "garrison", target: home.id });350 }351 }352353 // ------------------------------------------------------------------354 // Faith: spend on a warding rite over home while actually under attack.355 // One-shot spend; the tick-stamp guard spaces castings out.356 // ------------------------------------------------------------------357 if (358 state.faith >= 100 &&359 underAttack() &&360 (memory.wardAt === undefined || state.tick - memory.wardAt > 20)361 ) {362 api.rite("warding", home.id);363 memory.wardAt = state.tick;364 api.log("bulwark: warding rite over the walls");365 }366}367`;368369export const warlordSource = `// Warlord — aggressive reference bot for Tribes v2.370// @description: Raiders. Weapons-first smithy, early muster, drilled warbands that loot the fat and besiege the weak.371//372// Strategy: the economy exists to feed the smithy and the smithy exists to373// arm warbands. Lean extraction, weapons-first craft split, early muster,374// then raid the nearest enemy settlement — escalating to a siege once a band375// is heavy enough. Targeting is event-driven: 'sighted' and 'battle' events376// plus live enemySettlements sightings pick the victim; with no target the377// bands patrol seeded-random waypoints (api.rng — deterministic per seed).378//379// Contract: define a global \`function tick(state, memory, api)\`.380381function tick(state, memory, api) {382 // Prestige: try to advance the age when the hoard allows (the engine383 // drops the order if we cannot afford it — a cheap periodic attempt).384 if (state.tick % 50 === 0 && state.age < 3) api.advance();385386 var home = state.settlements[0];387 if (!home) return;388389 // Herd management (hysteresis): slaughter only while the flock is fat —390 // hides feed leather and the age hoard — and let a thin flock recover.391 var herds = home.herds || 0;392 if (!memory.slaughterOn && herds >= 16) {393 api.decree("slaughter", 80);394 memory.slaughterOn = 1;395 } else if (memory.slaughterOn && herds < 8) {396 api.decree("slaughter", 0);397 memory.slaughterOn = 0;398 }399400 function hasOrQueued(building) {401 if (home.buildings[building]) return true;402 for (var i = 0; i < home.buildQueue.length; i++) {403 if (home.buildQueue[i].building === building) return true;404 }405 return false;406 }407408 function manhattan(x1, y1, x2, y2) {409 var dx = x1 > x2 ? x1 - x2 : x2 - x1;410 var dy = y1 > y2 ? y1 - y2 : y2 - y1;411 return dx + dy;412 }413414 // Standing + queued count of a building at home.415 function countOf(building) {416 var n = home.buildings[building] || 0;417 for (var ci = 0; ci < home.buildQueue.length; ci++) {418 if (home.buildQueue[ci].building === building) n++;419 }420 return n;421 }422423 // ------------------------------------------------------------------424 // One-time policy init (persistent). Enough food to grow, ore/metal425 // heavy, weapons-first smithy. Sums to 940 permille.426 // ------------------------------------------------------------------427 if (!memory.init) {428 memory.init = 1;429 api.allocate("farmer", 310);430 api.allocate("fisher", 80);431 api.allocate("woodcutter", 160); // timber first: no camp, no smithy, no war432 api.allocate("miner", 60); // ore for the smelter (once a mine exists)433 api.allocate("miller", 40);434 api.allocate("baker", 60);435 api.allocate("smelter", 80);436 api.allocate("smith", 70); // the point of everything437 api.allocate("builder", 90);438 api.allocate("scholar", 40); // research gates the war chain439 api.allocate("hauler", 30);440 // Weapons before comfort; just enough tools to keep the mines fed.441 api.craft("tools", 250);442 api.craft("weapons", 600);443 api.craft("armor", 150);444 api.craft("mill", 800);445 api.craft("brew", 0);446 api.craft("fodder", 200); // horses later, maybe447 api.allocate("herder", 30);448 api.decree("rations", 1000);449 api.decree("expansion", 1000); // all ground within reach is ours // hungry soldiers desert — feed the war machine450 api.say("warlord: sharpen everything");451 return;452 }453454 // ------------------------------------------------------------------455 // Research: smelting -> weaponsmithing -> siegecraft. Metal, then456 // better weapons, then the means to crack walls.457 // ------------------------------------------------------------------458 if (state.researching === null) {459 if (state.techs.indexOf("smelting") < 0) api.research("smelting");460 else if (state.techs.indexOf("weaponsmithing") < 0) api.research("weaponsmithing");461 else if (state.techs.indexOf("siegecraft") < 0) api.research("siegecraft");462 }463464 // ------------------------------------------------------------------465 // Construction: the war chain only. Smithy above all.466 // ------------------------------------------------------------------467 if (home.buildQueue.length === 0) {468 // Growth targets scale with population: more mouths need more farms,469 // more people need more roofs. countOf() checks standing + queued.470 var wantFarms = 1 + Math.floor(home.pop / 60);471 var wantHouses = Math.floor(home.pop / 45);472 var wantCamps = 1 + Math.floor(home.pop / 150);473 if (countOf("farm") < wantFarms) api.build("farm", home.id);474 else if (countOf("lumber_camp") < wantCamps) api.build("lumber_camp", home.id);475 else if (countOf("pasture") < 1 + Math.floor(home.pop / 500)) api.build("pasture", home.id);476 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);477 else if (countOf("house") < wantHouses) api.build("house", home.id);478 }479 // ------------------------------------------------------------------480 // Ore or death: if our claims hold no hills (a neighbor beat us to481 // them), FOUND a mining outpost by the nearest highland and route its482 // ore home — the war chain cannot exist without metal.483 // ------------------------------------------------------------------484 if (485 state.settlements.length < 2 &&486 home.pop >= 500 &&487 (memory.foundTry === undefined || state.tick - memory.foundTry > 100)488 ) {489 var best = null;490 var bestD = -1;491 var size = state.mapSize;492 for (var ti = 0; ti < state.terrain.length; ti++) {493 // unclaimed explored highland, or a distant highland silhouette ('^')494 var isHill = false;495 var ch = state.terrain[ti];496 if ((ch === "h" || ch === "O" || ch === "m") && state.claims[ti] === ".") isHill = true;497 else if (state.distant[ti] === "^") isHill = true;498 if (!isHill) continue;499 var tx = ti % size;500 var ty = (ti / size) | 0;501 // the engine refuses camps within 6 of ANY settlement — keep margin502 var tooClose = false;503 if (Math.max(Math.abs(tx - home.x), Math.abs(ty - home.y)) < 6) tooClose = true;504 for (var es = 0; es < state.enemySettlements.length && !tooClose; es++) {505 var e = state.enemySettlements[es];506 if (Math.max(Math.abs(tx - e.x), Math.abs(ty - e.y)) < 6) tooClose = true;507 }508 if (tooClose) continue;509 var d = Math.max(Math.abs(tx - home.x), Math.abs(ty - home.y));510 if (best === null || d < bestD) { best = { x: tx, y: ty }; bestD = d; }511 }512 if (best !== null) {513 // camp on the nearest open ground beside the highland514 var cx = best.x;515 var cy = best.y;516 for (var r = 1; r <= 3 && state.terrain[cy * size + cx] !== "." ; r++) {517 for (var dy = -r; dy <= r; dy++) {518 for (var dx = -r; dx <= r; dx++) {519 var nx = best.x + dx;520 var ny = best.y + dy;521 if (nx < 0 || ny < 0 || nx >= size || ny >= size) continue;522 var nch = state.terrain[ny * size + nx];523 if (nch === "." || nch === "t") { cx = nx; cy = ny; dy = r + 1; break; }524 }525 }526 }527 // only pack what the stores can actually spare — unaffordable supplies void the order528 var pack = {};529 var wagon = { grain: 150, timber: 80, stone: 20, tools: 8 };530 for (var g in wagon) {531 var have = home.stores[g] || 0;532 if (have > wagon[g] * 2) pack[g] = wagon[g];533 else if (have > 20) pack[g] = (have / 2) | 0;534 }535 api.found(cx, cy, 80, pack);536 memory.foundTry = state.tick; // retry every 100t until the camp exists537 if (!memory.saidHills) {538 memory.saidHills = 1;539 api.say("warlord: the hills will be ours");540 }541 }542 }543 // Mines wherever hills are held; ore flows home for the smelter.544 if (state.tick % 40 === 0) {545 for (var si = 0; si < state.settlements.length; si++) {546 var stl = state.settlements[si];547 if (!stl.buildings.mine && stl.buildQueue.length < 2) api.build("mine", stl.id);548 if (stl.id !== home.id && !memory["route" + stl.id]) {549 api.route(stl.id, home.id, "ore", 700);550 api.route(stl.id, home.id, "stone", 600);551 memory["route" + stl.id] = 1;552 }553 }554 }555556 // The strategic plan advances IN PARALLEL with growth (a short queue always557 // has room for one plan item, or growth would crowd it out forever).558 if (home.buildQueue.length < 2) {559 var plan = ["library", "mine", "smelter", "smithy", "barracks", "granary"];560 for (var p = 0; p < plan.length; p++) {561 if (!hasOrQueued(plan[p])) {562 api.build(plan[p], home.id);563 break;564 }565 }566 }567568 // ------------------------------------------------------------------569 // Target selection, event-driven:570 // 1. Live sighting of an enemy settlement -> nearest one is the target.571 // 2. A battle event or enemy 'sighted' event -> remember the location572 // as a hunting ground to patrol (sightings decay after 60 ticks).573 // ------------------------------------------------------------------574 var target = null; // { id, x, y } enemy settlement575 var bestD = -1;576 for (var s = 0; s < state.enemySettlements.length; s++) {577 var es = state.enemySettlements[s];578 var d = manhattan(home.x, home.y, es.x, es.y);579 if (target === null || d < bestD) {580 target = es;581 bestD = d;582 }583 }584 if (target !== null) {585 // Refresh the grudge and make the hostility formal (persistent stance).586 if (memory.foe !== target.tribe) {587 memory.foe = target.tribe;588 api.stance(target.tribe, "hostile");589 api.log("warlord: marked tribe " + target.tribe + " hostile");590 }591 memory.huntX = target.x;592 memory.huntY = target.y;593 memory.huntUntil = state.tick + 60;594 } else {595 for (var e = 0; e < state.events.length; e++) {596 var ev = state.events[e];597 if (ev.t === "sighted" && ev.tribe === state.tribeId) {598 memory.huntX = ev.x;599 memory.huntY = ev.y;600 memory.huntUntil = state.tick + 60;601 } else if (ev.t === "battle" && ev.attacker !== state.tribeId) {602 memory.huntX = ev.x;603 memory.huntY = ev.y;604 memory.huntUntil = state.tick + 60;605 }606 }607 }608609 // ------------------------------------------------------------------610 // Muster: raise infantry bands (up to 3) sized to whatever the armory611 // can actually equip (starter stock is small; the smithy chain grows it).612 // Bands drill after mustering, so raise them EARLY and let them train613 // while the economy builds. Tick-stamp guard against re-muster spam.614 // ------------------------------------------------------------------615 var armory = home.stores.weapons || 0;616 var bandSize = armory >= 30 ? 30 : armory;617 if (618 state.warbands.length < 3 &&619 bandSize >= 15 &&620 (memory.musterAt === undefined || state.tick - memory.musterAt > 10)621 ) {622 api.muster(home.id, bandSize, "infantry");623 memory.musterAt = state.tick;624 }625626 // ------------------------------------------------------------------627 // Command loop. With a live settlement target: light bands raid it628 // (loot, no long commitment), heavy bands (50+) settle in to besiege.629 // No target: patrol the last hunting ground, else a seeded-random630 // waypoint re-rolled every 40 ticks.631 // ------------------------------------------------------------------632 for (var w = 0; w < state.warbands.length; w++) {633 var band = state.warbands[w];634 if (band.drilling > 0) {635 // Green troops train at home before campaigning (raid/besiege would636 // be refused anyway; don't waste the order budget).637 if (band.order.type !== "garrison") {638 api.command(band.id, { type: "garrison", target: band.home });639 }640 } else if (target !== null) {641 var wanted = band.size >= 50 ? "besiege" : "raid";642 if (band.order.type !== wanted || band.order.target !== target.id) {643 api.command(band.id, { type: wanted, target: target.id });644 }645 } else if (memory.huntUntil !== undefined && state.tick < memory.huntUntil) {646 if (band.order.type !== "patrol") {647 api.command(band.id, { type: "patrol", target: { x: memory.huntX, y: memory.huntY } });648 }649 } else {650 if (memory.scoutUntil === undefined || state.tick >= memory.scoutUntil) {651 memory.scoutX = Math.floor(api.rng() * state.mapSize);652 memory.scoutY = Math.floor(api.rng() * state.mapSize);653 memory.scoutUntil = state.tick + 40;654 }655 var pt = band.order.target;656 var onWaypoint =657 band.order.type === "patrol" &&658 pt !== null &&659 typeof pt === "object" &&660 pt.x === memory.scoutX &&661 pt.y === memory.scoutY;662 if (!onWaypoint) {663 api.command(band.id, { type: "patrol", target: { x: memory.scoutX, y: memory.scoutY } });664 }665 }666 }667}668`;669670export const magnateSource = `// Magnate — trade-focused reference bot for Tribes v2.671// @description: Merchant princes: fish and ale exports, coinage, tribute over war, escorted caravans.672//673// Strategy: get rich, stay friendly, pay off anyone who looks dangerous.674// The economy is fish + ale (cheap calories in, premium drink out), with an675// early market and the coinage tech to monetize it. Standing repeat offers676// sell ale and fish to anyone; tribes that attack us (or that we've had to677// mark hostile) get a one-shot coin tribute instead of a war. Small infantry678// bands exist only to escort our caravans.679//680// Contract: define a global \`function tick(state, memory, api)\`.681682function tick(state, memory, api) {683 // Prestige: try to advance the age when the hoard allows (the engine684 // drops the order if we cannot afford it — a cheap periodic attempt).685 if (state.tick % 50 === 0 && state.age < 3) api.advance();686687 var home = state.settlements[0];688 if (!home) return;689690 // Herd management (hysteresis): slaughter only while the flock is fat —691 // hides feed leather and the age hoard — and let a thin flock recover.692 var herds = home.herds || 0;693 if (!memory.slaughterOn && herds >= 16) {694 api.decree("slaughter", 80);695 memory.slaughterOn = 1;696 } else if (memory.slaughterOn && herds < 8) {697 api.decree("slaughter", 0);698 memory.slaughterOn = 0;699 }700701 function hasOrQueued(building) {702 if (home.buildings[building]) return true;703 for (var i = 0; i < home.buildQueue.length; i++) {704 if (home.buildQueue[i].building === building) return true;705 }706 return false;707 }708709 // Standing + queued count of a building at home.710 function countOf(building) {711 var n = home.buildings[building] || 0;712 for (var ci = 0; ci < home.buildQueue.length; ci++) {713 if (home.buildQueue[ci].building === building) n++;714 }715 return n;716 }717718 // ------------------------------------------------------------------719 // Phased labor policy (persistent). A ten-soul camp has no dock and no720 // customers: farm and build first, then pivot to the merchant diet —721 // fish for the town, grain for the brewery, haulers for the roads.722 // ------------------------------------------------------------------723 if (!memory.phase) {724 memory.phase = 1;725 api.allocate("farmer", 620);726 api.allocate("woodcutter", 200);727 api.allocate("builder", 150);728 api.craft("mill", 800);729 api.craft("brew", 150);730 api.craft("fodder", 50);731 api.allocate("herder", 30);732 api.craft("tools", 700);733 api.craft("weapons", 300);734 api.decree("rations", 1000);735 api.decree("expansion", 550); // build tall-ish - trade fills the gap736 api.say("magnate: everything has a price");737 return;738 }739 if (memory.phase === 1 && home.pop >= 150) {740 memory.phase = 2;741 // No water in reach means no dock, ever — the brewery drinks from the742 // fields instead. Checked once, here, before the merchant diet is set.743 memory.hasWater = 0;744 var wsize = state.mapSize;745 for (var wy = Math.max(0, home.y - 8); wy <= Math.min(wsize - 1, home.y + 8); wy++) {746 for (var wx = Math.max(0, home.x - 8); wx <= Math.min(wsize - 1, home.x + 8); wx++) {747 if (state.terrain[wy * wsize + wx] === "w") { memory.hasWater = 1; break; }748 }749 if (memory.hasWater) break;750 }751 api.allocate("fisher", memory.hasWater ? 260 : 0); // the food engine752 api.allocate("farmer", memory.hasWater ? 220 : 480); // grain mostly for the brewery753 api.allocate("woodcutter", 80);754 api.allocate("miner", 40);755 api.allocate("miller", 30);756 api.allocate("baker", 40);757 api.allocate("brewer", 130); // the profit engine758 api.allocate("smelter", 20);759 api.allocate("smith", 30);760 api.allocate("builder", 70);761 api.allocate("hauler", 60); // caravans and market flow762 api.allocate("scholar", 30); // toward coinage763 // Grain split: mostly to the brewery; smithy keeps tools coming with a764 // sliver of weapons for the escort bands.765 api.craft("mill", 350);766 api.craft("brew", 600);767 api.craft("fodder", 50);768 api.craft("tools", 700);769 api.craft("weapons", 250);770 api.craft("armor", 50);771 api.decree("ale_ration", 200); // a happy town drinks a little772 return;773 }774775 // ------------------------------------------------------------------776 // Research: brewing (the product), then coinage (the point), then777 // scholarship to keep the knowledge flowing.778 // ------------------------------------------------------------------779 if (state.researching === null) {780 if (state.techs.indexOf("brewing") < 0) api.research("brewing");781 else if (state.techs.indexOf("coinage") < 0) api.research("coinage");782 else if (state.techs.indexOf("scholarship") < 0) api.research("scholarship");783 }784785 // ------------------------------------------------------------------786 // Construction: dock -> brewery -> market as fast as possible; then787 // comfort and storage.788 // ------------------------------------------------------------------789 if (home.buildQueue.length === 0) {790 // Growth targets scale with population: more mouths need more farms,791 // more people need more roofs. countOf() checks standing + queued.792 var wantFarms = 1 + Math.floor(home.pop / 60);793 var wantHouses = Math.floor(home.pop / 45);794 var wantCamps = 1 + Math.floor(home.pop / 150);795 if (countOf("farm") < wantFarms) api.build("farm", home.id);796 else if (countOf("lumber_camp") < wantCamps) api.build("lumber_camp", home.id);797 else if (countOf("pasture") < 1 + Math.floor(home.pop / 500)) api.build("pasture", home.id);798 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);799 else if (countOf("house") < wantHouses) api.build("house", home.id);800 }801 // The strategic plan advances IN PARALLEL with growth (a short queue always802 // has room for one plan item, or growth would crowd it out forever).803 if (home.buildQueue.length < 2) {804 // No water in reach means no dock — ever. Check once, or the plan805 // stalls forever on an unbuildable first step.806 var plan = ["dock", "brewery", "market", "granary", "warehouse", "library", "shrine"];807 if (memory.hasWater === 0) plan.shift(); // landlocked (checked at phase 2)808 for (var p = 0; p < plan.length; p++) {809 if (!hasOrQueued(plan[p])) {810 api.build(plan[p], home.id);811 break;812 }813 }814 }815816 // ------------------------------------------------------------------817 // Standing trade offers (repeat, open to anyone non-hostile). Post each818 // product line once we actually hold stock and no such offer stands.819 // ------------------------------------------------------------------820 var sellingAle = false;821 var sellingFish = false;822 for (var t = 0; t < state.offers.length; t++) {823 var offer = state.offers[t];824 if (offer.tribe !== state.tribeId) continue;825 if (offer.give === "ale") sellingAle = true;826 if (offer.give === "fish") sellingFish = true;827 }828 // The tick-stamp guard gives the engine time to register a fresh offer in829 // state.offers before we conclude it's missing and re-post.830 if (memory.offerAt === undefined || state.tick - memory.offerAt > 10) {831 if (!sellingAle && (home.stores.ale || 0) >= 30) {832 api.trade({ give: "ale", giveAmount: 15, want: "coin", wantAmount: 10, tribe: null, repeat: true });833 memory.offerAt = state.tick;834 }835 if (!sellingFish && (home.stores.fish || 0) >= 60) {836 api.trade({ give: "fish", giveAmount: 30, want: "coin", wantAmount: 8, tribe: null, repeat: true });837 memory.offerAt = state.tick;838 }839 }840841 // ------------------------------------------------------------------842 // Tribute: any tribe that attacked us last tick (or that stands hostile843 // in our books) gets a one-shot coin gift — wantAmount 0 asks nothing844 // back. Cheaper than an army. At most one per tribe per 200 ticks.845 // ------------------------------------------------------------------846 if (!memory.tributed) memory.tributed = {};847 if ((home.stores.coin || 0) >= 60) {848 var menace = null;849 for (var e = 0; e < state.events.length; e++) {850 var ev = state.events[e];851 if (ev.t === "battle" && ev.defender === state.tribeId && ev.attacker !== state.tribeId) {852 menace = ev.attacker;853 } else if (ev.t === "raid" && ev.tribe === state.tribeId) {854 menace = ev.by;855 }856 }857 if (menace === null) {858 // No fresh aggression: appease the lowest-id tribe we consider hostile.859 for (var a = 0; a < state.aliveTribes.length; a++) {860 var other = state.aliveTribes[a];861 if (other !== state.tribeId && state.stances[other] === "hostile") {862 menace = other;863 break;864 }865 }866 }867 if (menace !== null && menace !== state.tribeId) {868 var last = memory.tributed[menace];869 if (last === undefined || state.tick - last > 200) {870 memory.tributed[menace] = state.tick;871 api.trade({ give: "coin", giveAmount: 25, want: "coin", wantAmount: 0, tribe: menace, repeat: false });872 api.log("magnate: tribute of 25 coin to tribe " + menace);873 }874 }875 }876877 // ------------------------------------------------------------------878 // Escorts: two small infantry bands guarding our caravans (or home when879 // nothing is on the road). Tick-stamp guard against re-muster spam.880 // ------------------------------------------------------------------881 if (882 state.warbands.length < 2 &&883 (home.stores.weapons || 0) >= 12 &&884 (memory.musterAt === undefined || state.tick - memory.musterAt > 10)885 ) {886 api.muster(home.id, 12, "infantry");887 memory.musterAt = state.tick;888 }889 var ownCaravans = [];890 for (var c = 0; c < state.caravans.length; c++) {891 if (state.caravans[c].tribe === state.tribeId) ownCaravans.push(state.caravans[c]);892 }893 for (var w = 0; w < state.warbands.length; w++) {894 var band = state.warbands[w];895 var caravan = ownCaravans[w % (ownCaravans.length || 1)];896 if (ownCaravans.length > 0 && caravan !== undefined) {897 if (band.order.type !== "escort" || band.order.target !== caravan.id) {898 api.command(band.id, { type: "escort", target: caravan.id });899 }900 } else if (band.order.type !== "garrison") {901 api.command(band.id, { type: "garrison", target: home.id });902 }903 }904}905`;906907export const primitivesSource = `// Primitives — frozen helper library for Tribes strategy scripts.908//909// This is NOT a runnable bot: it defines plain helper functions only.910// Fork it: copy any helpers you want ABOVE your \`tick\` function and edit911// freely. Scripts are self-contained by design — there is no injected912// stdlib — so whatever you ship in your source is exactly what replays913// forever (your script hash pins its full behavior).914//915// Every helper is deterministic: no clocks, no ambient randomness. Anything916// random takes \`api.rng\` explicitly. Ties always break toward the lowest917// index/id so replays never diverge.918919// Manhattan distance between two {x, y} points (all sim math is integer).920function dist(a, b) {921 var dx = a.x > b.x ? a.x - b.x : b.x - a.x;922 var dy = a.y > b.y ? a.y - b.y : b.y - a.y;923 return dx + dy;924}925926// Total population across every settlement you hold.927function totalPop(view) {928 var pop = 0;929 for (var i = 0; i < view.settlements.length; i++) pop += view.settlements[i].pop;930 return pop;931}932933// Total stored amount of one resource across every settlement.934// Example: storesTotal(view, "bread")935function storesTotal(view, resource) {936 var total = 0;937 for (var i = 0; i < view.settlements.length; i++) {938 var n = view.settlements[i].stores[resource];939 if (typeof n === "number") total += n;940 }941 return total;942}943944// Bulk labor-policy setter: one api.allocate per entry, in the plan's own945// key order (object literal order — deterministic). Allocations are946// PERSISTENT policy, so call this once from a memory-guarded init block,947// not every tick. Example:948// allocationPlan(api, { farmer: 300, woodcutter: 120, builder: 80 });949function allocationPlan(api, plan) {950 var keys = Object.keys(plan);951 for (var i = 0; i < keys.length; i++) api.allocate(keys[i], plan[keys[i]]);952}953954// Production-chain health: per-mille estimate of tool coverage for your955// extractive workforce (extraction runs at reduced yield without tools).956// Estimated from what the view exposes: tools in store vs. workers currently957// apportioned to extraction professions. 1000 = fully covered.958function chainHealth(view) {959 var extractive = ["farmer", "fisher", "herder", "woodcutter", "miner"];960 var workers = 0;961 for (var i = 0; i < view.settlements.length; i++) {962 var w = view.settlements[i].workers;963 for (var k = 0; k < extractive.length; k++) {964 var n = w[extractive[k]];965 if (typeof n === "number") workers += n;966 }967 }968 if (workers === 0) return 1000;969 var tools = storesTotal(view, "tools");970 var permille = ((tools * 1000) / workers) | 0;971 return permille > 1000 ? 1000 : permille;972}973974// Nearest visible monster camp to (x, y), or null. Ties break toward the975// earliest threats[] index.976function nearestThreat(view, x, y) {977 var best = null;978 var bestD = -1;979 for (var i = 0; i < view.threats.length; i++) {980 var d = dist(view.threats[i], { x: x, y: y });981 if (best === null || d < bestD) {982 best = view.threats[i];983 bestD = d;984 }985 }986 return best;987}988989// Your settlement with the given id, or null.990function settlementById(view, id) {991 for (var i = 0; i < view.settlements.length; i++) {992 if (view.settlements[i].id === id) return view.settlements[i];993 }994 return null;995}996997// True when last tick's events show us taking hits: a battle we defended, a998// raid or building loss inflicted on us, or one of our caravans robbed.999function underAttack(view) {1000 var me = view.tribeId;1001 for (var i = 0; i < view.events.length; i++) {1002 var e = view.events[i];1003 if (e.t === "battle" && e.defender === me) return true;1004 if (e.t === "raid" && e.tribe === me) return true;1005 if (e.t === "caravan_raided" && e.tribe === me) return true;1006 if (e.t === "building_destroyed" && e.tribe === me && e.by !== null) return true;1007 }1008 return false;1009}10101011// Broadcast \`text\` at most once per unique \`key\`, ever (flag persisted in1012// memory). Respects the one-say-per-tick cap by being called sparingly.1013function sayOnce(memory, api, key, text) {1014 if (!memory.__said) memory.__said = {};1015 if (memory.__said[key]) return false;1016 memory.__said[key] = 1;1017 api.say(text);1018 return true;1019}10201021// Advance a sequential build plan WITHOUT freezing on impossible steps.1022// \`plan\` is an array of building names in priority order (repeats allowed:1023// ["farm", "house", "farm"] wants two farms and a house). Each call issues1024// at most one build order and returns the building it tried, or null.1025// If the last attempt came back as an \`order_dropped\` event, the step is1026// skipped for \`cooldown\` ticks (default 200) instead of blocking the plan —1027// the single most common way scripts freeze is a plan stalled forever on1028// one silently-impossible building.1029function planBuilder(view, memory, api, plan, cooldown) {1030 if (!memory.__planSkip) memory.__planSkip = {};1031 var home = view.settlements[0];1032 if (!home) return null;1033 var wait = cooldown === undefined ? 200 : cooldown;1034 // Note any drop reports for builds we attempted.1035 for (var i = 0; i < view.events.length; i++) {1036 var e = view.events[i];1037 if (e.t === "order_dropped" && e.tribe === view.tribeId && e.order === "build" && memory.__planLast) {1038 memory.__planSkip[memory.__planLast] = view.tick + wait;1039 }1040 }1041 if (home.buildQueue.length > 0) return null; // one thing at a time1042 var counts = {};1043 for (var p = 0; p < plan.length; p++) {1044 var b = plan[p];1045 counts[b] = (counts[b] || 0) + 1;1046 var have = home.buildings[b] || 0;1047 for (var q = 0; q < home.buildQueue.length; q++) {1048 if (home.buildQueue[q].building === b) have++;1049 }1050 if (have >= counts[b]) continue;1051 if ((memory.__planSkip[b] || 0) > view.tick) continue; // blocked recently1052 api.build(b, home.id);1053 memory.__planLast = b;1054 return b;1055 }1056 return null;1057}1058`;10591060export const referenceBots = { homesteader: homesteaderSource, bulwark: bulwarkSource, warlord: warlordSource, magnate: magnateSource } as const;