clone

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

feat: settlement smoke + spawn distance ceiling — tribes actually meet

Dami committed 2b9f6ca
Measured on scattered spawns: peaceful 1v1s NEVER met (6/6 sampled) and
FFA pairs only met when a warband marched — contact was 100% military.
Two fixes: (1) settlement smoke — a settlement announces itself at
12/18/26/34 tiles by tier (visible from any of your settlements/warbands,
regardless of terrain vision; fires sighted events and appears in
enemySettlements). Units stay fogged. (2) spawn scoring gains a
nearest-neighbor CEILING at ~45% of map size alongside the 18-tile floor,
so arenas vary without stranding anyone past every horizon.

Remeasured: 1v1 6/6 met (~t1500, when camps become towns); FFA 29/36
pairs met (19% never — inside the <=20% target), every tribe meeting at
least one neighbor in every sim. Goldens repinned; trade scenario
re-seeded (70). Dev only — ships to prod with the next release.
commit
2b9f6ca4b1a7171f3340653b8b4eddd928786e35
parent
109806d1f1d09d461817db532d44af5aeb2f0be5
author
Dami <dami.etoile@gmail.com>
6 files changed, +38 -11
Mdocs/llms.txt2+-
Mpackages/engine/src/constants.ts4++++
Mpackages/engine/src/mapgen.ts12+++++++++---
Mpackages/engine/src/world.ts21+++++++++++++++++++--
Mpackages/engine/test/engine.test.ts8++++----
Mworker/src/mcp/docs.ts2+-
M docs/llms.txt +1 -1
M packages/engine/src/constants.ts +4 -0
M packages/engine/src/mapgen.ts +9 -3
M packages/engine/src/world.ts +19 -2
@@ -3069,6 +3069,22 @@ class World implements EngineWorld {
30693069 return mask;30703070 }30713071 3072+ /** True when `t`'s smoke is within sight of any of `obs`'s eyes: the3073+ * column reads at SMOKE_RANGE[tier] from the observer's settlements and3074+ * warbands, regardless of terrain vision. */3075+ private smokeVisible(obs: number, t: Settlement): boolean {3076+ const range = C.SMOKE_RANGE[t.tier]!;3077+ for (const mine of this.settlements) {3078+ if (mine.tribe !== obs) continue;3079+ if (cheb(mine.x, mine.y, t.x, t.y) <= range) return true;3080+ }3081+ for (const w of this.warbands) {3082+ if (w.tribe !== obs) continue;3083+ if (cheb(w.x, w.y, t.x, t.y) <= range) return true;3084+ }3085+ return false;3086+ }3087+30723088 private emitSightings(): void {30733089 const n = this.tribes.length;30743090 const s = this.size;
@@ -3092,7 +3108,7 @@ class World implements EngineWorld {
30923108 if (sx === -1) {30933109 for (const t of this.settlements) {30943110 if (t.tribe !== enemy) continue;3095- if (mask[t.y * s + t.x] === 1) {3111+ if (mask[t.y * s + t.x] === 1 || this.smokeVisible(obs, t)) {30963112 sx = t.x;30973113 sy = t.y;30983114 break;
@@ -3265,7 +3281,8 @@ class World implements EngineWorld {
32653281 const enemySettlements: EnemySettlementSighting[] = [];32663282 for (const t of this.settlements) {32673283 if (t.tribe === tribeId) continue;3268- if (visible[t.y * s + t.x] === 1) {3284+ // Direct vision, or the smoke column on the horizon.3285+ if (visible[t.y * s + t.x] === 1 || this.smokeVisible(tribeId, t)) {32693286 enemySettlements.push({32703287 tribe: t.tribe,32713288 id: t.id,
M packages/engine/test/engine.test.ts +4 -4
M worker/src/mcp/docs.ts +1 -1