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>
| M | docs/ | 2 | +- |
| M | packages/ | 4 | ++++ |
| M | packages/ | 12 | +++++++++--- |
| M | packages/ | 21 | +++++++++++++++++++-- |
| M | packages/ | 8 | ++++---- |
| M | worker/ | 2 | +- |
@@ -415,7 +415,7 @@ territory means buying metal or taking someone else's.
415415 | `warbands` | `WarbandView[]` | Your warbands. |416416 | `caravans` | `CaravanView[]` | Own + visible foreign caravans. |417417 | `threats` | `ThreatView[]` | Visible monster camps. |418-| `enemySettlements` | `EnemySettlementSighting[]` | Enemy settlements in vision. |418+| `enemySettlements` | `EnemySettlementSighting[]` | Enemy settlements in vision — or announced by their SMOKE: a settlement's smoke column is visible at 12/18/26/34 tiles (by tier) from any of your settlements or warbands, far past terrain vision. Civilizations cannot hide; units can. |419419 | `enemyWarbands` | `EnemyWarbandSighting[]` | Enemy warbands in vision. |420420 | `offers` | `TradeOfferView[]` | Own standing offers + foreign offers visible via your market. |421421 | `techs` | `Tech[]` | Unlocked techs. |@@ -613,6 +613,10 @@ export const DISTANT_VISION_BONUS = 5;
613613 /** Terrain code -> DistantChar category ('.' open, 'f' vegetation, '^' highland, 'w' water). */614614 export const DISTANT_CHARS = '.f^^wwf..^^^';615615 export const SIGHT_COOLDOWN = 200;616+/** A settlement's smoke column is visible far beyond terrain vision —617+ * camp/village/town/city by tier. Civilizations cannot hide: tribe-level618+ * awareness is near-inevitable as neighbors grow, while units stay fogged. */619+export const SMOKE_RANGE = [12, 18, 26, 34] as const;616620 617621 // ---------------------------------------------------------------------------618622 // Score@@ -226,11 +226,17 @@ export function generateMap(size: number, tribeCount: number, rng: Rng): Generat
226226 if (d < near) near = d;227227 }228228 const t = terrain[idx(x, y)]!;229- // Separation floor first (two fairness radii + margin, so terraforms230- // never pave each other), then keep spreading; water heavily penalized.229+ // Separation floor first (two fairness radii, so terraforms never230+ // pave each other), then spread — but with a nearest-neighbor CEILING:231+ // past ~45% of the map the reward flips to a penalty, so arenas vary232+ // without ever stranding tribes beyond each other's horizons.231233 const SEP = 18;234+ const CAP = Math.max(SEP + 2, Math.trunc((size * 45) / 100));232235 const score =233- Math.min(near, SEP) * 1024 + near * 4 - (t === TC.WATER ? size * 1024 : 0);236+ Math.min(near, SEP) * 1024 +237+ Math.min(near, CAP) * 4 -238+ Math.max(0, near - CAP) * 8 -239+ (t === TC.WATER ? size * 1024 : 0);234240 if (score > bestScore) {235241 bestScore = score;236242 bestX = x;@@ -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,@@ -262,9 +262,9 @@ const GOLDEN_CONFIG = { seed: '123456789', tribeCount: 4, mapSize: 128, maxTicks
262262 // simulation behavior must regenerate these (run this test — the failure263263 // output prints the actual hashes to pin).264264 const GOLDEN_HASHES: Record<number, string> = {265- 0: '19128df86a935bd7',266- 100: '732dc4313c61a5e6',267- 1000: '41c666e4faf0b903',265+ 0: '4f5d30fc489c614f',266+ 100: '89997759733352e2',267+ 1000: 'aa502849f515a1f6',268268 };269269 270270 describe('golden replay', () => {@@ -762,7 +762,7 @@ function growToMarkets(world: EngineWorld, tribes: number[]): void {
762762 763763 describe('trade', () => {764764 it('matches complementary offers at the midpoint and delivers by caravan', () => {765- const world = createWorld({ seed: '41', tribeCount: 2, mapSize: 64, maxTicks: 10_000 });765+ const world = createWorld({ seed: '70', tribeCount: 2, mapSize: 64, maxTicks: 10_000 });766766 // The humble start has no markets and near-empty stores: trade only767767 // works between grown economies.768768 growToMarkets(world, [0, 1]);@@ -425,7 +425,7 @@ territory means buying metal or taking someone else's.
425425 | \`warbands\` | \`WarbandView[]\` | Your warbands. |426426 | \`caravans\` | \`CaravanView[]\` | Own + visible foreign caravans. |427427 | \`threats\` | \`ThreatView[]\` | Visible monster camps. |428-| \`enemySettlements\` | \`EnemySettlementSighting[]\` | Enemy settlements in vision. |428+| \`enemySettlements\` | \`EnemySettlementSighting[]\` | Enemy settlements in vision — or announced by their SMOKE: a settlement's smoke column is visible at 12/18/26/34 tiles (by tier) from any of your settlements or warbands, far past terrain vision. Civilizations cannot hide; units can. |429429 | \`enemyWarbands\` | \`EnemyWarbandSighting[]\` | Enemy warbands in vision. |430430 | \`offers\` | \`TradeOfferView[]\` | Own standing offers + foreign offers visible via your market. |431431 | \`techs\` | \`Tech[]\` | Unlocked techs. |