diff --git a/src/games/civilization/CivilizationAI.js b/src/games/civilization/CivilizationAI.js index c52e05c..69acaec 100644 --- a/src/games/civilization/CivilizationAI.js +++ b/src/games/civilization/CivilizationAI.js @@ -21,7 +21,6 @@ import { considerRequest, considerGift, makeTreatyRequest, resolveRequest, aiWouldAccept, escalateFrustration, checkPledges, pickFairTrade, setCooldown, } from './CivilizationDiplomacy.js'; -import { siteQuality } from './CivilizationWorldGen.js'; const MAX_UNIT_STEPS = 40; // per unit per turn, guards against loops @@ -437,11 +436,50 @@ function stepSettler(rules, state, civIdx, unit, strategy) { return moveToward(rules, state, unit, best.x, best.y); } +function aiCitySiteScore(rules, world, x, y, covered) { + // Score the full working radius (Chebyshev distance <= 2), but only count + // tiles that aren't already within range of another city. + const { cols, rows } = world; + let q = 0; + for (let dy = -2; dy <= 2; dy += 1) { + for (let dx = -2; dx <= 2; dx += 1) { + const nx = x + dx; + const ny = y + dy; + if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue; + if (covered.has(nx * 1000 + ny)) continue; // tile already covered by another city + const terr = rules.terrainList[world.terrain[ny * cols + nx]]; + if (terr.water || terr.id === 'mountains' || terr.id === 'glacier') continue; + const spec = world.special[ny * cols + nx] >= 0 ? rules.specialList[world.special[ny * cols + nx]] : null; + const food = spec ? spec.food : terr.food; + const shield = spec ? spec.shield : terr.shield; + const trade = spec ? spec.trade : terr.trade; + q += food * 3 + shield * 2 + trade; + } + } + return q; +} + export function bestCitySite(rules, state, civIdx, unit) { const { world } = state; + const myCities = civCities(state, civIdx); let best = null; let bestScore = -Infinity; - const R = 7; + const R = 12; // search wider so distant good sites are found + + // Pre-compute which tiles are already within working radius of own cities + const covered = new Set(); + for (const city of myCities) { + for (let dy = -2; dy <= 2; dy += 1) { + for (let dx = -2; dx <= 2; dx += 1) { + const cx = city.x + dx; + const cy = city.y + dy; + if (cx >= 0 && cy >= 0 && cx < world.cols && cy < world.rows) { + covered.add(cx * 1000 + cy); + } + } + } + } + for (let dy = -R; dy <= R; dy += 1) { for (let dx = -R; dx <= R; dx += 1) { const x = unit.x + dx; @@ -450,10 +488,25 @@ export function bestCitySite(rules, state, civIdx, unit) { if (!canFoundCity(rules, state, x, y)) continue; const terr = terrainAt(rules, world, x, y); if (terr.water || terr.id === 'mountains') continue; + + // Don't found right on top of a neighbour's doorstep const enemyClose = state.civs.some((c) => c.alive && c.id !== civIdx && civCities(state, c.id).some((ct) => cheb(ct.x, ct.y, x, y) <= 3)); if (enemyClose) continue; - const score = siteQuality(rules, world, x, y) - cheb(unit.x, unit.y, x, y) * 2; + + // Penalize sites too close to own cities (overlapping working radii) + let tooClose = false; + let minOwnDist = Infinity; + for (const city of myCities) { + const d = cheb(city.x, city.y, x, y); + if (d < minOwnDist) minOwnDist = d; + if (d <= 3) { tooClose = true; break; } // hard reject: radii overlap almost fully + } + if (tooClose) continue; + + const score = aiCitySiteScore(rules, world, x, y, covered) + - cheb(unit.x, unit.y, x, y) * 1.5 + - (minOwnDist <= 5 ? (5 - minOwnDist) * 6 : 0); // soft penalty at 4-5 tiles if (score > bestScore) { bestScore = score; best = { x, y }; } } }