1// Bulwark — defensive reference bot for Tribes v2.2// @description: Fortress doctrine: stone walls, towers, armored garrisons, warding rites. Sells ale, buys metal, outlasts raiders.3//4// Strategy: dig in and outlast. Rush masonry, stack walls and towers, and5// keep a large armored garrison at home. Faith goes into warding rites when6// we are actually being hit. The economy leans on stone/ore extraction and7// pays for imported metal by selling surplus ale (a standing repeat offer).8// Never attacks; every warband holds the walls.9//10// Contract: define a global `function tick(state, memory, api)`.1112function tick(state, memory, api) {13 // Prestige: try to advance the age when the hoard allows (the engine14 // drops the order if we cannot afford it — a cheap periodic attempt).15 if (state.tick % 50 === 0 && state.age < 3) api.advance();1617 var home = state.settlements[0];18 if (!home) return;1920 // Herd management (hysteresis): slaughter only while the flock is fat —21 // hides feed leather and the age hoard — and let a thin flock recover.22 var herds = home.herds || 0;23 if (!memory.slaughterOn && herds >= 16) {24 api.decree("slaughter", 80);25 memory.slaughterOn = 1;26 } else if (memory.slaughterOn && herds < 8) {27 api.decree("slaughter", 0);28 memory.slaughterOn = 0;29 }3031 function hasOrQueued(building) {32 if (home.buildings[building]) return true;33 for (var i = 0; i < home.buildQueue.length; i++) {34 if (home.buildQueue[i].building === building) return true;35 }36 return false;37 }3839 // Did last tick's events show us taking hits?40 function underAttack() {41 for (var i = 0; i < state.events.length; i++) {42 var e = state.events[i];43 if (e.t === "battle" && e.defender === state.tribeId) return true;44 if (e.t === "raid" && e.tribe === state.tribeId) return true;45 if (e.t === "building_destroyed" && e.tribe === state.tribeId && e.by !== null) return true;46 }47 return false;48 }4950 // Standing + queued count of a building at home.51 function countOf(building) {52 var n = home.buildings[building] || 0;53 for (var ci = 0; ci < home.buildQueue.length; ci++) {54 if (home.buildQueue[ci].building === building) n++;55 }56 return n;57 }5859 // ------------------------------------------------------------------60 // Phased labor policy (persistent). Ten souls cannot run a fortress:61 // the founding camp farms and builds; the fortress diet — stone, ore,62 // steel — waits until there are backs enough to carry it.63 // ------------------------------------------------------------------64 if (!memory.phase) {65 memory.phase = 1;66 api.allocate("farmer", 620);67 api.allocate("woodcutter", 200);68 api.allocate("builder", 150); // roofs first, ramparts later69 api.craft("tools", 700);70 api.craft("weapons", 300);71 api.craft("mill", 800);72 api.craft("brew", 150);73 api.craft("fodder", 50);74 api.allocate("herder", 30);75 api.decree("rations", 1000);76 api.decree("expansion", 700); // walls first, acreage second77 api.say("bulwark: stone outlasts steel");78 return;79 }80 if (memory.phase === 1 && home.pop >= 150) {81 memory.phase = 2;82 api.allocate("farmer", 300);83 api.allocate("woodcutter", 90);84 api.allocate("miner", 150); // stone + ore: the fortress diet85 api.allocate("miller", 40);86 api.allocate("baker", 50);87 api.allocate("brewer", 60); // ale surplus is our trade good88 api.allocate("smelter", 60);89 api.allocate("smith", 60);90 api.allocate("builder", 90); // the wall never stops rising91 api.allocate("scholar", 40); // masonry does not research itself92 api.allocate("priest", 30); // faith for warding rites93 // Smithy split: garrison gets steel on its back, not just in hand.94 api.craft("tools", 400);95 api.craft("weapons", 300);96 api.craft("armor", 300);97 api.craft("mill", 700);98 api.craft("brew", 250);99 return;100 }101102 // ------------------------------------------------------------------103 // Research: masonry first (walls), then castellany (fortification),104 // then plate_armor for the garrison.105 // ------------------------------------------------------------------106 if (state.researching === null) {107 if (state.techs.indexOf("masonry") < 0) api.research("masonry");108 else if (state.techs.indexOf("castellany") < 0) api.research("castellany");109 else if (state.techs.indexOf("plate_armor") < 0) api.research("plate_armor");110 }111112 // ------------------------------------------------------------------113 // Construction: fortifications first, economy second. Walls stack (the114 // count is the wall level), so keep raising them to 3; towers to 4.115 // One queued job at a time so the builders never thrash.116 // ------------------------------------------------------------------117 if (home.buildQueue.length === 0) {118 // Growth targets scale with population: more mouths need more farms,119 // more people need more roofs. countOf() checks standing + queued.120 var wantFarms = 1 + Math.floor(home.pop / 60);121 var wantHouses = Math.floor(home.pop / 45);122 var wantCamps = 1 + Math.floor(home.pop / 150);123 if (countOf("farm") < wantFarms) api.build("farm", home.id);124 else if (countOf("lumber_camp") < wantCamps) api.build("lumber_camp", home.id);125 else if (countOf("pasture") < 1 + Math.floor(home.pop / 500)) api.build("pasture", home.id);126 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);127 else if (countOf("house") < wantHouses) api.build("house", home.id);128 else if (!hasOrQueued("granary")) api.build("granary", home.id);129 }130 // Fortifications advance IN PARALLEL with growth.131 if (home.buildQueue.length < 2) {132 var walls = home.buildings.walls || 0;133 var towers = home.buildings.tower || 0;134 var stone = home.stores.stone || 0;135 if (!hasOrQueued("library")) api.build("library", home.id);136 else if (!hasOrQueued("mine")) api.build("mine", home.id);137 // affordability guards: an unaffordable step must not stall the chain138 else if (!hasOrQueued("smelter") && stone >= 40) api.build("smelter", home.id);139 else if (!hasOrQueued("smithy") && stone >= 20) api.build("smithy", home.id);140 else if (walls < 1) api.build("walls", home.id);141 else if (!hasOrQueued("barracks")) api.build("barracks", home.id);142 else if (!hasOrQueued("brewery")) api.build("brewery", home.id);143 else if (walls < 3 && (home.stores.stone || 0) >= 80) api.build("walls", home.id);144 else if (towers < 4 && (home.stores.stone || 0) >= 60) api.build("tower", home.id);145 else if (!hasOrQueued("shrine")) api.build("shrine", home.id);146 }147148 // ------------------------------------------------------------------149 // Trade: one standing repeat offer — surplus ale for metal, open to any150 // non-hostile tribe. Only (re)posted when we hold no such offer.151 // ------------------------------------------------------------------152 var haveAleOffer = false;153 for (var t = 0; t < state.offers.length; t++) {154 var offer = state.offers[t];155 if (offer.tribe === state.tribeId && offer.give === "ale") haveAleOffer = true;156 }157 if (158 !haveAleOffer &&159 (home.stores.ale || 0) >= 40 &&160 (memory.offerAt === undefined || state.tick - memory.offerAt > 10)161 ) {162 api.trade({ give: "ale", giveAmount: 20, want: "metal", wantAmount: 8, tribe: null, repeat: true });163 memory.offerAt = state.tick; // let the engine register it before re-posting164 }165166 // ------------------------------------------------------------------167 // Garrison: up to two 30-strong infantry bands as weapons allow, all168 // parked on garrison duty at home. Tick-stamp guard against re-muster169 // spam while the engine raises the band.170 // ------------------------------------------------------------------171 var armory = home.stores.weapons || 0;172 if (173 state.warbands.length < 2 &&174 home.pop >= 120 &&175 armory >= 12 &&176 (memory.musterAt === undefined || state.tick - memory.musterAt > 10)177 ) {178 api.muster(home.id, Math.min(40, armory), "infantry");179 memory.musterAt = state.tick;180 }181 for (var w = 0; w < state.warbands.length; w++) {182 var band = state.warbands[w];183 if (band.order.type !== "garrison") {184 api.command(band.id, { type: "garrison", target: home.id });185 }186 }187188 // ------------------------------------------------------------------189 // Faith: spend on a warding rite over home while actually under attack.190 // One-shot spend; the tick-stamp guard spaces castings out.191 // ------------------------------------------------------------------192 if (193 state.faith >= 100 &&194 underAttack() &&195 (memory.wardAt === undefined || state.tick - memory.wardAt > 20)196 ) {197 api.rite("warding", home.id);198 memory.wardAt = state.tick;199 api.log("bulwark: warding rite over the walls");200 }201}