1// Magnate — trade-focused reference bot for Tribes v2.2// @description: Merchant princes: fish and ale exports, coinage, tribute over war, escorted caravans.3//4// Strategy: get rich, stay friendly, pay off anyone who looks dangerous.5// The economy is fish + ale (cheap calories in, premium drink out), with an6// early market and the coinage tech to monetize it. Standing repeat offers7// sell ale and fish to anyone; tribes that attack us (or that we've had to8// mark hostile) get a one-shot coin tribute instead of a war. Small infantry9// bands exist only to escort our caravans.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 // Standing + queued count of a building at home.41 function countOf(building) {42 var n = home.buildings[building] || 0;43 for (var ci = 0; ci < home.buildQueue.length; ci++) {44 if (home.buildQueue[ci].building === building) n++;45 }46 return n;47 }4849 // ------------------------------------------------------------------50 // Phased labor policy (persistent). A ten-soul camp has no dock and no51 // customers: farm and build first, then pivot to the merchant diet —52 // fish for the town, grain for the brewery, haulers for the roads.53 // ------------------------------------------------------------------54 if (!memory.phase) {55 memory.phase = 1;56 api.allocate("farmer", 620);57 api.allocate("woodcutter", 200);58 api.allocate("builder", 150);59 api.craft("mill", 800);60 api.craft("brew", 150);61 api.craft("fodder", 50);62 api.allocate("herder", 30);63 api.craft("tools", 700);64 api.craft("weapons", 300);65 api.decree("rations", 1000);66 api.decree("expansion", 550); // build tall-ish - trade fills the gap67 api.say("magnate: everything has a price");68 return;69 }70 if (memory.phase === 1 && home.pop >= 150) {71 memory.phase = 2;72 // No water in reach means no dock, ever — the brewery drinks from the73 // fields instead. Checked once, here, before the merchant diet is set.74 memory.hasWater = 0;75 var wsize = state.mapSize;76 for (var wy = Math.max(0, home.y - 8); wy <= Math.min(wsize - 1, home.y + 8); wy++) {77 for (var wx = Math.max(0, home.x - 8); wx <= Math.min(wsize - 1, home.x + 8); wx++) {78 if (state.terrain[wy * wsize + wx] === "w") { memory.hasWater = 1; break; }79 }80 if (memory.hasWater) break;81 }82 api.allocate("fisher", memory.hasWater ? 260 : 0); // the food engine83 api.allocate("farmer", memory.hasWater ? 220 : 480); // grain mostly for the brewery84 api.allocate("woodcutter", 80);85 api.allocate("miner", 40);86 api.allocate("miller", 30);87 api.allocate("baker", 40);88 api.allocate("brewer", 130); // the profit engine89 api.allocate("smelter", 20);90 api.allocate("smith", 30);91 api.allocate("builder", 70);92 api.allocate("hauler", 60); // caravans and market flow93 api.allocate("scholar", 30); // toward coinage94 // Grain split: mostly to the brewery; smithy keeps tools coming with a95 // sliver of weapons for the escort bands.96 api.craft("mill", 350);97 api.craft("brew", 600);98 api.craft("fodder", 50);99 api.craft("tools", 700);100 api.craft("weapons", 250);101 api.craft("armor", 50);102 api.decree("ale_ration", 200); // a happy town drinks a little103 return;104 }105106 // ------------------------------------------------------------------107 // Research: brewing (the product), then coinage (the point), then108 // scholarship to keep the knowledge flowing.109 // ------------------------------------------------------------------110 if (state.researching === null) {111 if (state.techs.indexOf("brewing") < 0) api.research("brewing");112 else if (state.techs.indexOf("coinage") < 0) api.research("coinage");113 else if (state.techs.indexOf("scholarship") < 0) api.research("scholarship");114 }115116 // ------------------------------------------------------------------117 // Construction: dock -> brewery -> market as fast as possible; then118 // comfort and storage.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 // No water in reach means no dock — ever. Check once, or the plan136 // stalls forever on an unbuildable first step.137 var plan = ["dock", "brewery", "market", "granary", "warehouse", "library", "shrine"];138 if (memory.hasWater === 0) plan.shift(); // landlocked (checked at phase 2)139 for (var p = 0; p < plan.length; p++) {140 if (!hasOrQueued(plan[p])) {141 api.build(plan[p], home.id);142 break;143 }144 }145 }146147 // ------------------------------------------------------------------148 // Standing trade offers (repeat, open to anyone non-hostile). Post each149 // product line once we actually hold stock and no such offer stands.150 // ------------------------------------------------------------------151 var sellingAle = false;152 var sellingFish = false;153 for (var t = 0; t < state.offers.length; t++) {154 var offer = state.offers[t];155 if (offer.tribe !== state.tribeId) continue;156 if (offer.give === "ale") sellingAle = true;157 if (offer.give === "fish") sellingFish = true;158 }159 // The tick-stamp guard gives the engine time to register a fresh offer in160 // state.offers before we conclude it's missing and re-post.161 if (memory.offerAt === undefined || state.tick - memory.offerAt > 10) {162 if (!sellingAle && (home.stores.ale || 0) >= 30) {163 api.trade({ give: "ale", giveAmount: 15, want: "coin", wantAmount: 10, tribe: null, repeat: true });164 memory.offerAt = state.tick;165 }166 if (!sellingFish && (home.stores.fish || 0) >= 60) {167 api.trade({ give: "fish", giveAmount: 30, want: "coin", wantAmount: 8, tribe: null, repeat: true });168 memory.offerAt = state.tick;169 }170 }171172 // ------------------------------------------------------------------173 // Tribute: any tribe that attacked us last tick (or that stands hostile174 // in our books) gets a one-shot coin gift — wantAmount 0 asks nothing175 // back. Cheaper than an army. At most one per tribe per 200 ticks.176 // ------------------------------------------------------------------177 if (!memory.tributed) memory.tributed = {};178 if ((home.stores.coin || 0) >= 60) {179 var menace = null;180 for (var e = 0; e < state.events.length; e++) {181 var ev = state.events[e];182 if (ev.t === "battle" && ev.defender === state.tribeId && ev.attacker !== state.tribeId) {183 menace = ev.attacker;184 } else if (ev.t === "raid" && ev.tribe === state.tribeId) {185 menace = ev.by;186 }187 }188 if (menace === null) {189 // No fresh aggression: appease the lowest-id tribe we consider hostile.190 for (var a = 0; a < state.aliveTribes.length; a++) {191 var other = state.aliveTribes[a];192 if (other !== state.tribeId && state.stances[other] === "hostile") {193 menace = other;194 break;195 }196 }197 }198 if (menace !== null && menace !== state.tribeId) {199 var last = memory.tributed[menace];200 if (last === undefined || state.tick - last > 200) {201 memory.tributed[menace] = state.tick;202 api.trade({ give: "coin", giveAmount: 25, want: "coin", wantAmount: 0, tribe: menace, repeat: false });203 api.log("magnate: tribute of 25 coin to tribe " + menace);204 }205 }206 }207208 // ------------------------------------------------------------------209 // Escorts: two small infantry bands guarding our caravans (or home when210 // nothing is on the road). Tick-stamp guard against re-muster spam.211 // ------------------------------------------------------------------212 if (213 state.warbands.length < 2 &&214 (home.stores.weapons || 0) >= 12 &&215 (memory.musterAt === undefined || state.tick - memory.musterAt > 10)216 ) {217 api.muster(home.id, 12, "infantry");218 memory.musterAt = state.tick;219 }220 var ownCaravans = [];221 for (var c = 0; c < state.caravans.length; c++) {222 if (state.caravans[c].tribe === state.tribeId) ownCaravans.push(state.caravans[c]);223 }224 for (var w = 0; w < state.warbands.length; w++) {225 var band = state.warbands[w];226 var caravan = ownCaravans[w % (ownCaravans.length || 1)];227 if (ownCaravans.length > 0 && caravan !== undefined) {228 if (band.order.type !== "escort" || band.order.target !== caravan.id) {229 api.command(band.id, { type: "escort", target: caravan.id });230 }231 } else if (band.order.type !== "garrison") {232 api.command(band.id, { type: "garrison", target: home.id });233 }234 }235}