clone

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

feat: territory as strategy — expansion decree, living borders, reachable ages

Dami committed d5920e4
Territory is a choice, not physics. New decree('expansion', 0-1000,
default 500): the base camp footprint is automatic, but all reach beyond
it — tier, age, and population (+1 claim budget per 25 of isqrt(pop),
capped) — is scaled by the decree. Isolationists stay deliberately small;
manifest destiny claims every acre and borders keep creeping as the
civilization grows, so the territory chart no longer flatlines at tier 3.

Ages were unreachable in practice: age 1 needs 100 hides, hides come from
herd slaughter, and the founding herds were culled at the first slow tick
for lack of pasture. The hall now shelters a small founding flock (6 herd
+ 2 herder slots), and the reference bots manage herds with hysteresis
(slaughter fat flocks, spare thin ones), build pastures with pop, and
express territorial identity through the expansion dial.

Docs updated (decree table, territory-as-choice section, pasture note);
goldens repinned; test harness opts into full expansion explicitly.
commit
d5920e46d0a04c5f311abc8da289de4f73077ff1
parent
7e8e1f813443346dd0091d102622be90fb2416d7
author
Dami <dami.etoile@gmail.com>
13 files changed, +196 -34
Mdocs/llms.txt26+++++++++-----
Mpackages/engine/src/constants.ts17++++++++-
Mpackages/engine/src/world.ts28+++++++++++---
Mpackages/engine/test/engine.test.ts7+++-
Mpackages/protocol/src/game.ts2+-
Mpackages/script-host/bots/bulwark.js14+++++++
Mpackages/script-host/bots/homesteader.js16++++++++-
Mpackages/script-host/bots/magnate.js14+++++++
Mpackages/script-host/bots/warlord.js16++++++++-
Mpackages/script-host/src/bots.ts60+++++++++++++++++++++++++++++-
Mpackages/script-host/src/index.ts2+-
Mweb/src/dev-mock.ts2+-
Mworker/src/mcp/docs.ts26+++++++++-----
M docs/llms.txt +17 -9
@@ -229,7 +229,7 @@ population growth.
229229 | `warehouse` | — | goods storage capacity (`storeCap`) |230230 | `farm` | — | farmer slots |231231 | `dock` | — | fisher slots (must touch water) |232-| `pasture` | — | herder slots; herds & horses live here |232+| `pasture` | — | herder slots; herds & horses live here (the hall shelters a founding flock of 6, so herds survive the early game — growth needs real pastures) |233233 | `lumber_camp` | — | woodcutter + carpenter slots |234234 | `mine` | — | miner slots (hills/mountain/veins); costs timber only, so stone always bootstraps |235235 | `mill` | milling | miller slots |
@@ -361,7 +361,7 @@ v2. One-shot verbs act once and are done. All are subject to the 64-order
361361 | `api.cancelTrade(offerId)` | `trade_cancel` | One-shot: withdraw one of your standing offers. |362362 | `api.stance(tribeId, posture)` | `stance` | **Policy.** `'ally' \| 'neutral' \| 'hostile'` toward a tribe. Gates trade matching and engagement rules. |363363 | `api.research(tech)` | `research` | Sets the research target (one at a time) until unlocked. |364-| `api.decree(key, value)` | `decree` | **Policy.** Civil per-mille knobs: `'rations'` (food issued per capita), `'slaughter'` (herd culling → meat/hides), `'fodder'` (livestock feeding), `'ale_ration'` (ale issue → morale). Echoed in `state.decrees`. |364+| `api.decree(key, value)` | `decree` | **Policy.** Civil per-mille knobs: `'rations'` (food issued per capita), `'slaughter'` (herd culling → meat/hides), `'fodder'` (livestock feeding), `'ale_ration'` (ale issue → morale), `'expansion'` (territorial appetite, default 500 — see Territory). Echoed in `state.decrees`. |365365 | `api.rite(rite, target)` | `rite` | One-shot **faith spend** (the exception to persistence): `'blessing' \| 'harvest' \| 'warding' \| 'war'`, `target` = settlement id or `null`. |366366 | `api.advance()` | one-shot | Prestige: burn the age hoard (escalating resource chunk drawn across your settlements; costs in the engine README) to enter the next age. Each age: +10% extraction & refinement rates, +3 warband power per soldier, a score term AND a x1.25-per-age score multiplier. Dropped silently if unaffordable. |367367 | `api.say(text)` | `say` | Broadcast a short message to EVERY tribe's feed (and spectators). Sanitized; 120 chars max, 1 per tick. |
@@ -619,13 +619,21 @@ function tick(state, memory, api) {
619619 620620 ### Territory (organic claims)621621 622-Settlements claim ground by cheapest-cost expansion, budgeted by tier: plains623-and steppe cost 1, forest/hills/arctic 2, swamp 3, mountains 5 — water,624-rivers and volcanoes cannot be claimed AT ALL. Rivers and coasts are hard625-borders; mountain lines are near-borders. Your claims cap your extraction626-(farmers need claimed plains, miners claimed hills). Contested frontiers627-resolve by expansion cost, earlier-founded settlements winning ties. Watch628-`state.claims` and `settlement.territoryTiles`.622+Settlements claim ground by cheapest-cost expansion: plains and steppe cost623+1, forest/hills/arctic 2, swamp 3, mountains 5 — water, rivers and volcanoes624+cannot be claimed AT ALL. Rivers and coasts are hard borders; mountain lines625+are near-borders. The expansion budget is NOT one-time capture — and it is a626+CHOICE. The base camp footprint is automatic, but everything beyond it (tier627+reach, age reach, population reach) is scaled by your `expansion` decree628+(0-1000, default 500). A nomadic or isolationist tribe stays deliberately629+small at `expansion 0` no matter how large it grows; a manifest-destiny630+tribe at `expansion 1000` claims every acre its tier, ages, and population631+can reach, and its borders keep creeping for as long as the civilization632+keeps growing. Territory feeds extraction but lengthens the border you must633+defend — playing tall (dense, compact, trade-fed) is a real strategy. Your claims cap your extraction (farmers need claimed plains,634+miners claimed hills). Contested frontiers resolve by expansion cost,635+earlier-founded settlements winning ties. Watch `state.claims` and636+`settlement.territoryTiles`.629637 630638 ### Ages (prestige)631639
M packages/engine/src/constants.ts +16 -1
@@ -157,6 +157,16 @@ export const TIER_RADIUS = [4, 6, 9, 12] as const;
157157 export const CLAIM_COST: ReadonlyArray<number> = [1, 2, 2, 5, 0, 0, 3, 1, 2, 0, 2, 2];158158 /** Max accumulated claim cost by settlement tier (expansion budget). */159159 export const TIER_CLAIM_BUDGET = [5, 8, 12, 16] as const;160+/** Claim budget added per tribe age: borders keep moving after tier 3. */161+export const AGE_CLAIM_BONUS = [0, 2, 3, 4] as const;162+/** Everything beyond the base camp footprint (TIER_CLAIM_BUDGET[0]) is163+ * scaled by the `expansion` decree (0-1000 permille, default 500): territory164+ * is a strategy, not physics. Isolationists keep a hamlet's borders at any165+ * size; expansionists claim the full tier + age + pop reach. */166+/** Claim budget added by settlement pop: +1 per 25 of isqrt(pop), capped.167+ * (pop 625 -> +1, 2500 -> +2, 5625 -> +3, 10000 -> +4, 15625 -> +5, 22500 -> +6) */168+export const POP_CLAIM_DIV = 25;169+export const POP_CLAIM_CAP = 6;160170 161171 // ---------------------------------------------------------------------------162172 // Ages (prestige ladder)
@@ -328,6 +338,11 @@ export const THREAT_ARSON_MIN_BUILDINGS = 6;
328338 export const HALL_GARDEN_SLOTS = 12;329339 /** Hall salvage yard: baseline woodcutter slots so wreckage can be rebuilt. */330340 export const HALL_YARD_SLOTS = 6;341+/** The hall's herd yard: the founding flock survives without a pasture342+ * (real herding — growth past this — still needs pastures and herders). */343+export const HALL_HERD_SLOTS = 6;344+/** Herder slots the hall provides for that small yard. */345+export const HALL_HERDER_SLOTS = 2;331346 export const EMIGRATION_RATE = 10; // ‰ per 10t332347 /** Housing factor: 1000 below 80% occupancy, linear to 0 at 100%. */333348 export const HOUSING_SOFT_PERMILLE = 800;
@@ -642,4 +657,4 @@ export const START_STORES: Partial<Record<Resource, number>> = {
642657 /** Default craft splits (tools/weapons/armor; mill/brew/fodder). */643658 export const DEFAULT_CRAFT: readonly number[] = [600, 250, 150, 500, 100, 200];644659 /** Default decrees (rations/slaughter/fodder/ale_ration). */645-export const DEFAULT_DECREES: readonly number[] = [1000, 0, 0, 0];660+export const DEFAULT_DECREES: readonly number[] = [1000, 0, 0, 0, 500];
M packages/engine/src/world.ts +22 -6
@@ -67,7 +67,7 @@ const P_N = C.P_N;
6767 const B_N = C.B_N;6868 6969 const CRAFT_KEYS: readonly CraftTarget[] = ['tools', 'weapons', 'armor', 'mill', 'brew', 'fodder'];70-const DECREE_KEYS: readonly DecreeKey[] = ['rations', 'slaughter', 'fodder', 'ale_ration'];70+const DECREE_KEYS: readonly DecreeKey[] = ['rations', 'slaughter', 'fodder', 'ale_ration', 'expansion'];7171 const THREAT_KINDS: readonly ThreatKind[] = [7272 'goblin_camp',7373 'skeleton_barrow',
@@ -280,7 +280,7 @@ interface Tribe {
280280 age: number;281281 alloc: Int32Array; // P_N permille282282 craft: Int32Array; // 6 (CRAFT_KEYS order)283- decrees: Int32Array; // 4 (DECREE_KEYS order)283+ decrees: Int32Array; // 5 (DECREE_KEYS order)284284 stances: Int8Array; // tribeCount, POSTURES index285285 techMask: number;286286 researching: number; // TECHS index or -1
@@ -552,7 +552,10 @@ class World implements EngineWorld {
552552 const n = this.size * this.size;553553 this.claims.fill(-1);554554 const cost = new Int16Array(n).fill(32767);555- const maxBudget = C.TIER_CLAIM_BUDGET[C.TIER_CLAIM_BUDGET.length - 1]!;555+ const maxBudget =556+ C.TIER_CLAIM_BUDGET[C.TIER_CLAIM_BUDGET.length - 1]! +557+ C.AGE_CLAIM_BONUS[C.AGE_CLAIM_BONUS.length - 1]! +558+ C.POP_CLAIM_CAP;556559 const buckets: number[][] = [];557560 for (let i = 0; i <= maxBudget; i++) buckets.push([]);558561 const budgetOf = new Map<number, number>();
@@ -560,7 +563,20 @@ class World implements EngineWorld {
560563 const i = st.y * this.size + st.x;561564 this.claims[i] = st.id;562565 cost[i] = 0;563- budgetOf.set(st.id, C.TIER_CLAIM_BUDGET[st.tier]!);566+ // Territory is a CHOICE: the base camp footprint is automatic, but567+ // everything beyond it — tier, age, and population reach — is scaled568+ // by the tribe's `expansion` decree. Nomads and isolationists stay569+ // small on purpose; manifest destiny is a policy, not a side effect.570+ const tribe = this.tribes[st.tribe]!;571+ const popBonus = Math.min(572+ C.POP_CLAIM_CAP,573+ Math.trunc(Math.floor(Math.sqrt(st.pop)) / C.POP_CLAIM_DIV),574+ );575+ const base = C.TIER_CLAIM_BUDGET[0]!;576+ const reach =577+ C.TIER_CLAIM_BUDGET[st.tier]! - base + C.AGE_CLAIM_BONUS[tribe.age]! + popBonus;578+ const expansion = tribe.decrees[4]!;579+ budgetOf.set(st.id, base + Math.trunc((reach * expansion) / 1000));564580 buckets[0]!.push(i);565581 }566582 for (let c = 0; c <= maxBudget; c++) {
@@ -652,7 +668,7 @@ class World implements EngineWorld {
652668 c.plains * C.FARMERS_PER_PLAINS,653669 );654670 cap[PI.fisher] = Math.min(b[BI.dock]! * C.DOCK_SLOTS, c.water * C.FISHERS_PER_WATER);655- cap[PI.herder] = b[BI.pasture]! * C.PASTURE_HERDER_SLOTS;671+ cap[PI.herder] = b[BI.pasture]! * C.PASTURE_HERDER_SLOTS + C.HALL_HERDER_SLOTS;656672 cap[PI.woodcutter] = Math.min(657673 b[BI.lumber_camp]! * C.LUMBER_SLOTS + b[BI.hall]! * C.HALL_YARD_SLOTS,658674 c.forest * C.WOODCUTTERS_PER_FOREST,
@@ -1690,7 +1706,7 @@ class World implements EngineWorld {
16901706 for (const s of this.settlements) {16911707 const tribe = this.tribes[s.tribe]!;16921708 const pastures = s.buildings[BI.pasture]!;1693- const capHerds = pastures * C.HERDS_PER_PASTURE;1709+ const capHerds = pastures * C.HERDS_PER_PASTURE + C.HALL_HERD_SLOTS;16941710 const capHorses = pastures * C.HORSES_PER_PASTURE;16951711 const tended = Math.min(16961712 s.herds,
M packages/engine/test/engine.test.ts +5 -2
M packages/protocol/src/game.ts +1 -1
M packages/script-host/bots/bulwark.js +14 -0
@@ -17,6 +17,17 @@ function tick(state, memory, api) {
1717 var home = state.settlements[0];1818 if (!home) return;1919 20+ // 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+ }30+2031 function hasOrQueued(building) {2132 if (home.buildings[building]) return true;2233 for (var i = 0; i < home.buildQueue.length; i++) {
@@ -60,7 +71,9 @@ function tick(state, memory, api) {
6071 api.craft("mill", 800);6172 api.craft("brew", 150);6273 api.craft("fodder", 50);74+ api.allocate("herder", 30);6375 api.decree("rations", 1000);76+ api.decree("expansion", 700); // walls first, acreage second6477 api.say("bulwark: stone outlasts steel");6578 return;6679 }
@@ -109,6 +122,7 @@ function tick(state, memory, api) {
109122 var wantCamps = 1 + Math.floor(home.pop / 150);110123 if (countOf("farm") < wantFarms) api.build("farm", home.id);111124 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);112126 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);113127 else if (countOf("house") < wantHouses) api.build("house", home.id);114128 else if (!hasOrQueued("granary")) api.build("granary", home.id);
M packages/script-host/bots/homesteader.js +15 -1
@@ -21,6 +21,17 @@ function tick(state, memory, api) {
2121 var home = state.settlements[0];2222 if (!home) return; // eliminated-in-all-but-name; nothing to govern2323 24+ // Herd management (hysteresis): slaughter only while the flock is fat —25+ // hides feed leather and the age hoard — and let a thin flock recover.26+ var herds = home.herds || 0;27+ if (!memory.slaughterOn && herds >= 16) {28+ api.decree("slaughter", 80);29+ memory.slaughterOn = 1;30+ } else if (memory.slaughterOn && herds < 8) {31+ api.decree("slaughter", 0);32+ memory.slaughterOn = 0;33+ }34+2435 // True when `building` is already standing or in the build queue at home.2536 function hasOrQueued(building) {2637 if (home.buildings[building]) return true;
@@ -70,7 +81,9 @@ function tick(state, memory, api) {
7081 api.craft("mill", 800);7182 api.craft("brew", 100);7283 api.craft("fodder", 100);73- api.decree("rations", 1000); // full rations: morale + growth84+ api.allocate("herder", 30);85+ api.decree("rations", 1000);86+ api.decree("expansion", 1000); // every acre ploughed: the homestead sprawls // full rations: morale + growth7487 api.say("homesteader: tilling the good earth");7588 return; // first tick done — keep it cheap and under the action cap7689 }
@@ -109,6 +122,7 @@ function tick(state, memory, api) {
109122 var wantCamps = 1 + Math.floor(home.pop / 150);110123 if (countOf("farm") < wantFarms) api.build("farm", home.id);111124 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);112126 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);113127 else if (countOf("house") < wantHouses) api.build("house", home.id);114128 }
M packages/script-host/bots/magnate.js +14 -0
@@ -18,6 +18,17 @@ function tick(state, memory, api) {
1818 var home = state.settlements[0];1919 if (!home) return;2020 21+ // 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+ }31+2132 function hasOrQueued(building) {2233 if (home.buildings[building]) return true;2334 for (var i = 0; i < home.buildQueue.length; i++) {
@@ -48,9 +59,11 @@ function tick(state, memory, api) {
4859 api.craft("mill", 800);4960 api.craft("brew", 150);5061 api.craft("fodder", 50);62+ api.allocate("herder", 30);5163 api.craft("tools", 700);5264 api.craft("weapons", 300);5365 api.decree("rations", 1000);66+ api.decree("expansion", 550); // build tall-ish - trade fills the gap5467 api.say("magnate: everything has a price");5568 return;5669 }
@@ -112,6 +125,7 @@ function tick(state, memory, api) {
112125 var wantCamps = 1 + Math.floor(home.pop / 150);113126 if (countOf("farm") < wantFarms) api.build("farm", home.id);114127 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);115129 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);116130 else if (countOf("house") < wantHouses) api.build("house", home.id);117131 }
M packages/script-host/bots/warlord.js +15 -1
@@ -18,6 +18,17 @@ function tick(state, memory, api) {
1818 var home = state.settlements[0];1919 if (!home) return;2020 21+ // 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+ }31+2132 function hasOrQueued(building) {2233 if (home.buildings[building]) return true;2334 for (var i = 0; i < home.buildQueue.length; i++) {
@@ -65,7 +76,9 @@ function tick(state, memory, api) {
6576 api.craft("mill", 800);6677 api.craft("brew", 0);6778 api.craft("fodder", 200); // horses later, maybe68- api.decree("rations", 1000); // hungry soldiers desert — feed the war machine79+ 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 machine6982 api.say("warlord: sharpen everything");7083 return;7184 }
@@ -91,6 +104,7 @@ function tick(state, memory, api) {
91104 var wantCamps = 1 + Math.floor(home.pop / 150);92105 if (countOf("farm") < wantFarms) api.build("farm", home.id);93106 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);94108 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);95109 else if (countOf("house") < wantHouses) api.build("house", home.id);96110 }
M packages/script-host/src/bots.ts +58 -2
@@ -24,6 +24,17 @@ function tick(state, memory, api) {
2424 var home = state.settlements[0];2525 if (!home) return; // eliminated-in-all-but-name; nothing to govern2626 27+ // 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+ }37+2738 // True when \`building\` is already standing or in the build queue at home.2839 function hasOrQueued(building) {2940 if (home.buildings[building]) return true;
@@ -73,7 +84,9 @@ function tick(state, memory, api) {
7384 api.craft("mill", 800);7485 api.craft("brew", 100);7586 api.craft("fodder", 100);76- api.decree("rations", 1000); // full rations: morale + growth87+ api.allocate("herder", 30);88+ api.decree("rations", 1000);89+ api.decree("expansion", 1000); // every acre ploughed: the homestead sprawls // full rations: morale + growth7790 api.say("homesteader: tilling the good earth");7891 return; // first tick done — keep it cheap and under the action cap7992 }
@@ -112,6 +125,7 @@ function tick(state, memory, api) {
112125 var wantCamps = 1 + Math.floor(home.pop / 150);113126 if (countOf("farm") < wantFarms) api.build("farm", home.id);114127 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);115129 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);116130 else if (countOf("house") < wantHouses) api.build("house", home.id);117131 }
@@ -168,6 +182,17 @@ function tick(state, memory, api) {
168182 var home = state.settlements[0];169183 if (!home) return;170184 185+ // 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+ }195+171196 function hasOrQueued(building) {172197 if (home.buildings[building]) return true;173198 for (var i = 0; i < home.buildQueue.length; i++) {
@@ -211,7 +236,9 @@ function tick(state, memory, api) {
211236 api.craft("mill", 800);212237 api.craft("brew", 150);213238 api.craft("fodder", 50);239+ api.allocate("herder", 30);214240 api.decree("rations", 1000);241+ api.decree("expansion", 700); // walls first, acreage second215242 api.say("bulwark: stone outlasts steel");216243 return;217244 }
@@ -260,6 +287,7 @@ function tick(state, memory, api) {
260287 var wantCamps = 1 + Math.floor(home.pop / 150);261288 if (countOf("farm") < wantFarms) api.build("farm", home.id);262289 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);263291 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);264292 else if (countOf("house") < wantHouses) api.build("house", home.id);265293 else if (!hasOrQueued("granary")) api.build("granary", home.id);
@@ -358,6 +386,17 @@ function tick(state, memory, api) {
358386 var home = state.settlements[0];359387 if (!home) return;360388 389+ // 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+ }399+361400 function hasOrQueued(building) {362401 if (home.buildings[building]) return true;363402 for (var i = 0; i < home.buildQueue.length; i++) {
@@ -405,7 +444,9 @@ function tick(state, memory, api) {
405444 api.craft("mill", 800);406445 api.craft("brew", 0);407446 api.craft("fodder", 200); // horses later, maybe408- api.decree("rations", 1000); // hungry soldiers desert — feed the war machine447+ 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 machine409450 api.say("warlord: sharpen everything");410451 return;411452 }
@@ -431,6 +472,7 @@ function tick(state, memory, api) {
431472 var wantCamps = 1 + Math.floor(home.pop / 150);432473 if (countOf("farm") < wantFarms) api.build("farm", home.id);433474 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);434476 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);435477 else if (countOf("house") < wantHouses) api.build("house", home.id);436478 }
@@ -645,6 +687,17 @@ function tick(state, memory, api) {
645687 var home = state.settlements[0];646688 if (!home) return;647689 690+ // 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+ }700+648701 function hasOrQueued(building) {649702 if (home.buildings[building]) return true;650703 for (var i = 0; i < home.buildQueue.length; i++) {
@@ -675,9 +728,11 @@ function tick(state, memory, api) {
675728 api.craft("mill", 800);676729 api.craft("brew", 150);677730 api.craft("fodder", 50);731+ api.allocate("herder", 30);678732 api.craft("tools", 700);679733 api.craft("weapons", 300);680734 api.decree("rations", 1000);735+ api.decree("expansion", 550); // build tall-ish - trade fills the gap681736 api.say("magnate: everything has a price");682737 return;683738 }
@@ -739,6 +794,7 @@ function tick(state, memory, api) {
739794 var wantCamps = 1 + Math.floor(home.pop / 150);740795 if (countOf("farm") < wantFarms) api.build("farm", home.id);741796 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);742798 else if (home.pop >= 60 && countOf("library") < 1) api.build("library", home.id);743799 else if (countOf("house") < wantHouses) api.build("house", home.id);744800 }
M packages/script-host/src/index.ts +1 -1
M web/src/dev-mock.ts +1 -1
M worker/src/mcp/docs.ts +17 -9
@@ -239,7 +239,7 @@ population growth.
239239 | \`warehouse\` | — | goods storage capacity (\`storeCap\`) |240240 | \`farm\` | — | farmer slots |241241 | \`dock\` | — | fisher slots (must touch water) |242-| \`pasture\` | — | herder slots; herds & horses live here |242+| \`pasture\` | — | herder slots; herds & horses live here (the hall shelters a founding flock of 6, so herds survive the early game — growth needs real pastures) |243243 | \`lumber_camp\` | — | woodcutter + carpenter slots |244244 | \`mine\` | — | miner slots (hills/mountain/veins); costs timber only, so stone always bootstraps |245245 | \`mill\` | milling | miller slots |
@@ -371,7 +371,7 @@ v2. One-shot verbs act once and are done. All are subject to the 64-order
371371 | \`api.cancelTrade(offerId)\` | \`trade_cancel\` | One-shot: withdraw one of your standing offers. |372372 | \`api.stance(tribeId, posture)\` | \`stance\` | **Policy.** \`'ally' \\| 'neutral' \\| 'hostile'\` toward a tribe. Gates trade matching and engagement rules. |373373 | \`api.research(tech)\` | \`research\` | Sets the research target (one at a time) until unlocked. |374-| \`api.decree(key, value)\` | \`decree\` | **Policy.** Civil per-mille knobs: \`'rations'\` (food issued per capita), \`'slaughter'\` (herd culling → meat/hides), \`'fodder'\` (livestock feeding), \`'ale_ration'\` (ale issue → morale). Echoed in \`state.decrees\`. |374+| \`api.decree(key, value)\` | \`decree\` | **Policy.** Civil per-mille knobs: \`'rations'\` (food issued per capita), \`'slaughter'\` (herd culling → meat/hides), \`'fodder'\` (livestock feeding), \`'ale_ration'\` (ale issue → morale), \`'expansion'\` (territorial appetite, default 500 — see Territory). Echoed in \`state.decrees\`. |375375 | \`api.rite(rite, target)\` | \`rite\` | One-shot **faith spend** (the exception to persistence): \`'blessing' \\| 'harvest' \\| 'warding' \\| 'war'\`, \`target\` = settlement id or \`null\`. |376376 | \`api.advance()\` | one-shot | Prestige: burn the age hoard (escalating resource chunk drawn across your settlements; costs in the engine README) to enter the next age. Each age: +10% extraction & refinement rates, +3 warband power per soldier, a score term AND a x1.25-per-age score multiplier. Dropped silently if unaffordable. |377377 | \`api.say(text)\` | \`say\` | Broadcast a short message to EVERY tribe's feed (and spectators). Sanitized; 120 chars max, 1 per tick. |
@@ -629,13 +629,21 @@ function tick(state, memory, api) {
629629 630630 ### Territory (organic claims)631631 632-Settlements claim ground by cheapest-cost expansion, budgeted by tier: plains633-and steppe cost 1, forest/hills/arctic 2, swamp 3, mountains 5 — water,634-rivers and volcanoes cannot be claimed AT ALL. Rivers and coasts are hard635-borders; mountain lines are near-borders. Your claims cap your extraction636-(farmers need claimed plains, miners claimed hills). Contested frontiers637-resolve by expansion cost, earlier-founded settlements winning ties. Watch638-\`state.claims\` and \`settlement.territoryTiles\`.632+Settlements claim ground by cheapest-cost expansion: plains and steppe cost633+1, forest/hills/arctic 2, swamp 3, mountains 5 — water, rivers and volcanoes634+cannot be claimed AT ALL. Rivers and coasts are hard borders; mountain lines635+are near-borders. The expansion budget is NOT one-time capture — and it is a636+CHOICE. The base camp footprint is automatic, but everything beyond it (tier637+reach, age reach, population reach) is scaled by your \`expansion\` decree638+(0-1000, default 500). A nomadic or isolationist tribe stays deliberately639+small at \`expansion 0\` no matter how large it grows; a manifest-destiny640+tribe at \`expansion 1000\` claims every acre its tier, ages, and population641+can reach, and its borders keep creeping for as long as the civilization642+keeps growing. Territory feeds extraction but lengthens the border you must643+defend — playing tall (dense, compact, trade-fed) is a real strategy. Your claims cap your extraction (farmers need claimed plains,644+miners claimed hills). Contested frontiers resolve by expansion cost,645+earlier-founded settlements winning ties. Watch \`state.claims\` and646+\`settlement.territoryTiles\`.639647 640648 ### Ages (prestige)641649