clone

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

main
tribes / packages / script-host / bots / primitives.js
primitives.js 5.7K
1// Primitives — frozen helper library for Tribes strategy scripts.2//3// This is NOT a runnable bot: it defines plain helper functions only.4// Fork it: copy any helpers you want ABOVE your `tick` function and edit5// freely. Scripts are self-contained by design — there is no injected6// stdlib — so whatever you ship in your source is exactly what replays7// forever (your script hash pins its full behavior).8//9// Every helper is deterministic: no clocks, no ambient randomness. Anything10// random takes `api.rng` explicitly. Ties always break toward the lowest11// index/id so replays never diverge.1213// Manhattan distance between two {x, y} points (all sim math is integer).14function dist(a, b) {15 var dx = a.x > b.x ? a.x - b.x : b.x - a.x;16 var dy = a.y > b.y ? a.y - b.y : b.y - a.y;17 return dx + dy;18}1920// Total population across every settlement you hold.21function totalPop(view) {22 var pop = 0;23 for (var i = 0; i < view.settlements.length; i++) pop += view.settlements[i].pop;24 return pop;25}2627// Total stored amount of one resource across every settlement.28// Example: storesTotal(view, "bread")29function storesTotal(view, resource) {30 var total = 0;31 for (var i = 0; i < view.settlements.length; i++) {32 var n = view.settlements[i].stores[resource];33 if (typeof n === "number") total += n;34 }35 return total;36}3738// Bulk labor-policy setter: one api.allocate per entry, in the plan's own39// key order (object literal order — deterministic). Allocations are40// PERSISTENT policy, so call this once from a memory-guarded init block,41// not every tick. Example:42// allocationPlan(api, { farmer: 300, woodcutter: 120, builder: 80 });43function allocationPlan(api, plan) {44 var keys = Object.keys(plan);45 for (var i = 0; i < keys.length; i++) api.allocate(keys[i], plan[keys[i]]);46}4748// Production-chain health: per-mille estimate of tool coverage for your49// extractive workforce (extraction runs at reduced yield without tools).50// Estimated from what the view exposes: tools in store vs. workers currently51// apportioned to extraction professions. 1000 = fully covered.52function chainHealth(view) {53 var extractive = ["farmer", "fisher", "herder", "woodcutter", "miner"];54 var workers = 0;55 for (var i = 0; i < view.settlements.length; i++) {56 var w = view.settlements[i].workers;57 for (var k = 0; k < extractive.length; k++) {58 var n = w[extractive[k]];59 if (typeof n === "number") workers += n;60 }61 }62 if (workers === 0) return 1000;63 var tools = storesTotal(view, "tools");64 var permille = ((tools * 1000) / workers) | 0;65 return permille > 1000 ? 1000 : permille;66}6768// Nearest visible monster camp to (x, y), or null. Ties break toward the69// earliest threats[] index.70function nearestThreat(view, x, y) {71 var best = null;72 var bestD = -1;73 for (var i = 0; i < view.threats.length; i++) {74 var d = dist(view.threats[i], { x: x, y: y });75 if (best === null || d < bestD) {76 best = view.threats[i];77 bestD = d;78 }79 }80 return best;81}8283// Your settlement with the given id, or null.84function settlementById(view, id) {85 for (var i = 0; i < view.settlements.length; i++) {86 if (view.settlements[i].id === id) return view.settlements[i];87 }88 return null;89}9091// True when last tick's events show us taking hits: a battle we defended, a92// raid or building loss inflicted on us, or one of our caravans robbed.93function underAttack(view) {94 var me = view.tribeId;95 for (var i = 0; i < view.events.length; i++) {96 var e = view.events[i];97 if (e.t === "battle" && e.defender === me) return true;98 if (e.t === "raid" && e.tribe === me) return true;99 if (e.t === "caravan_raided" && e.tribe === me) return true;100 if (e.t === "building_destroyed" && e.tribe === me && e.by !== null) return true;101 }102 return false;103}104105// Broadcast `text` at most once per unique `key`, ever (flag persisted in106// memory). Respects the one-say-per-tick cap by being called sparingly.107function sayOnce(memory, api, key, text) {108 if (!memory.__said) memory.__said = {};109 if (memory.__said[key]) return false;110 memory.__said[key] = 1;111 api.say(text);112 return true;113}114115// Advance a sequential build plan WITHOUT freezing on impossible steps.116// `plan` is an array of building names in priority order (repeats allowed:117// ["farm", "house", "farm"] wants two farms and a house). Each call issues118// at most one build order and returns the building it tried, or null.119// If the last attempt came back as an `order_dropped` event, the step is120// skipped for `cooldown` ticks (default 200) instead of blocking the plan —121// the single most common way scripts freeze is a plan stalled forever on122// one silently-impossible building.123function planBuilder(view, memory, api, plan, cooldown) {124 if (!memory.__planSkip) memory.__planSkip = {};125 var home = view.settlements[0];126 if (!home) return null;127 var wait = cooldown === undefined ? 200 : cooldown;128 // Note any drop reports for builds we attempted.129 for (var i = 0; i < view.events.length; i++) {130 var e = view.events[i];131 if (e.t === "order_dropped" && e.tribe === view.tribeId && e.order === "build" && memory.__planLast) {132 memory.__planSkip[memory.__planLast] = view.tick + wait;133 }134 }135 if (home.buildQueue.length > 0) return null; // one thing at a time136 var counts = {};137 for (var p = 0; p < plan.length; p++) {138 var b = plan[p];139 counts[b] = (counts[b] || 0) + 1;140 var have = home.buildings[b] || 0;141 for (var q = 0; q < home.buildQueue.length; q++) {142 if (home.buildQueue[q].building === b) have++;143 }144 if (have >= counts[b]) continue;145 if ((memory.__planSkip[b] || 0) > view.tick) continue; // blocked recently146 api.build(b, home.id);147 memory.__planLast = b;148 return b;149 }150 return null;151}